diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7a3b2bc..095bb5b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,7 +72,7 @@ jobs: strategy: fail-fast: false matrix: - toolchain: [ nightly, beta, stable, 1.85.0 ] + toolchain: [ nightly, beta, stable, 1.87.0 ] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master diff --git a/Cargo.lock b/Cargo.lock index 58b1783..097b79b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,9 +201,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" +checksum = "08b5d4e069cbc868041a64bd68dc8cb39a0d79585cd6c5a24caa8c2d622121be" dependencies = [ "aws-lc-sys", "zeroize", @@ -211,9 +211,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" dependencies = [ "bindgen", "cc", @@ -250,19 +250,16 @@ dependencies = [ ] [[package]] -name = "base64" -version = "0.22.1" +name = "base58" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" [[package]] -name = "base85" -version = "2.0.0" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36915bbaca237c626689b5bd14d02f2ba7a5a359d30a2a08be697392e3718079" -dependencies = [ - "thiserror", -] +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bech32" @@ -342,8 +339,7 @@ checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" [[package]] name = "bp-consensus" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22bbb56809c40565d6085b4211ee2d25a7b5e84848960678ff748e0a8707552f" +source = "git+https://github.com/BP-WG/bp-core?branch=develop#89a85c069abfacfb56b40d25a24153e496dd55af" dependencies = [ "amplify", "chrono", @@ -359,8 +355,7 @@ dependencies = [ [[package]] name = "bp-core" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a28cb4d675dd49dddd705a29d868d3e3b8f217639fd6f9cbfcbf435236eef77" +source = "git+https://github.com/BP-WG/bp-core?branch=develop#89a85c069abfacfb56b40d25a24153e496dd55af" dependencies = [ "bp-consensus", "bp-dbc", @@ -376,11 +371,10 @@ dependencies = [ [[package]] name = "bp-dbc" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aa1f2d9e00a4f2b107d2df25e0d804cfb87a175a37ee503b982b5784e892a6d" +source = "git+https://github.com/BP-WG/bp-core?branch=develop#89a85c069abfacfb56b40d25a24153e496dd55af" dependencies = [ "amplify", - "base85", + "base58", "bp-consensus", "commit_verify", "getrandom 0.2.16", @@ -394,8 +388,7 @@ dependencies = [ [[package]] name = "bp-derive" version = "0.12.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9868177dbd5f58895b1e75487f51c3aa42b6362958d1a4f8fc17e4885cc668b3" +source = "git+https://github.com/BP-WG/bp-std#256d8214acf66bcaace28ad499513d31dc3d06f9" dependencies = [ "amplify", "bp-consensus", @@ -418,7 +411,7 @@ dependencies = [ "byteorder", "libc", "log", - "rustls 0.23.28", + "rustls 0.23.29", "serde", "serde_json", "sha2", @@ -447,8 +440,7 @@ dependencies = [ [[package]] name = "bp-invoice" version = "0.12.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b151a5d8bf1ce4867efd262f8a3aa383a243be7c41ecb825971cb032ea22c" +source = "git+https://github.com/BP-WG/bp-std#256d8214acf66bcaace28ad499513d31dc3d06f9" dependencies = [ "amplify", "bech32", @@ -461,10 +453,10 @@ dependencies = [ [[package]] name = "bp-seals" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "565ffa069d6425b01630c68dbf1088a88786bd4b794ebf1f37a1d6edbbc29817" +source = "git+https://github.com/BP-WG/bp-core?branch=develop#89a85c069abfacfb56b40d25a24153e496dd55af" dependencies = [ "amplify", + "base58", "bp-consensus", "bp-dbc", "commit_verify", @@ -479,8 +471,7 @@ dependencies = [ [[package]] name = "bp-std" version = "0.12.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c69f38f9656b94b7241a9219523306fd3ed1aaefe5bd931da079a86dfeda025" +source = "git+https://github.com/BP-WG/bp-std#256d8214acf66bcaace28ad499513d31dc3d06f9" dependencies = [ "amplify", "bp-consensus", @@ -516,9 +507,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.29" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "jobserver", "libc", @@ -772,8 +763,7 @@ dependencies = [ [[package]] name = "descriptors" version = "0.12.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d3679f0c2ca4e9c071e963c42695b6180b854ca0eba20cec8168363b169d1e0" +source = "git+https://github.com/BP-WG/bp-std#256d8214acf66bcaace28ad499513d31dc3d06f9" dependencies = [ "amplify", "bp-derive", @@ -1725,8 +1715,7 @@ dependencies = [ [[package]] name = "psbt" version = "0.12.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25159222184ff955caf54e457079287c3f0dc3ce40dc556d9f565ffeb5c8139e" +source = "git+https://github.com/BP-WG/bp-std#256d8214acf66bcaace28ad499513d31dc3d06f9" dependencies = [ "amplify", "base64", @@ -2094,15 +2083,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2119,15 +2108,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.28" +version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ "aws-lc-rs", "log", "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.3", + "rustls-webpki 0.103.4", "subtle", "zeroize", ] @@ -2153,9 +2142,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", "ring", @@ -2293,9 +2282,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", @@ -2644,30 +2633,10 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.7", + "rustix 1.0.8", "windows-sys 0.59.0", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "time" version = "0.3.41" @@ -3084,14 +3053,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.1", + "webpki-roots 1.0.2", ] [[package]] name = "webpki-roots" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -3355,9 +3324,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 3732d4d..fe9e08c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ categories = ["cryptography::cryptocurrencies"] authors = ["Dr Maxim Orlovsky "] homepage = "https://rgb.tech" repository = "https://github.com/RGB-WG/rgb" -rust-version = "1.85.0" +rust-version = "1.87.0" edition = "2021" license = "Apache-2.0" @@ -130,3 +130,14 @@ serde = [ [package.metadata.docs.rs] features = ["all"] + +[patch.crates-io] +bp-consensus = { git = "https://github.com/BP-WG/bp-core", branch = "develop" } +bp-dbc = { git = "https://github.com/BP-WG/bp-core", branch = "develop" } +bp-seals = { git = "https://github.com/BP-WG/bp-core", branch = "develop" } +bp-core = { git = "https://github.com/BP-WG/bp-core", branch = "develop" } +bp-invoice = { git = "https://github.com/BP-WG/bp-std" } +bp-derive = { git = "https://github.com/BP-WG/bp-std" } +descriptors = { git = "https://github.com/BP-WG/bp-std" } +psbt = { git = "https://github.com/BP-WG/bp-std" } +bp-std = { git = "https://github.com/BP-WG/bp-std" } diff --git a/descriptors/src/display.rs b/descriptors/src/display.rs new file mode 100644 index 0000000..3b6e61b --- /dev/null +++ b/descriptors/src/display.rs @@ -0,0 +1,216 @@ +// Wallet Library for RGB smart contracts +// +// SPDX-License-Identifier: Apache-2.0 +// +// Designed in 2019-2025 by Dr Maxim Orlovsky +// Written in 2024-2025 by Dr Maxim Orlovsky +// +// Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. +// Copyright (C) 2024-2025 LNP/BP Laboratories, +// Institute for Distributed and Cognitive Systems (InDCS), Switzerland. +// Copyright (C) 2025 RGB Consortium, Switzerland. +// Copyright (C) 2019-2025 Dr Maxim Orlovsky. +// All rights under the above copyrights are reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. + +use core::fmt; +use core::fmt::{Display, Formatter}; + +use crate::{SealDescr, TapretTweaks}; + +impl Display for SealDescr { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("seals(")?; + let mut iter = self.0.iter().peekable(); + while let Some(seal) = iter.next() { + Display::fmt(seal, f)?; + if iter.peek().is_some() { + f.write_str(",")?; + } + } + f.write_str(")") + } +} + +impl Display for TapretTweaks { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("tweaks(")?; + let mut iter1 = self.iter().peekable(); + while let Some((term, tweaks)) = iter1.next() { + if tweaks.is_empty() { + continue; + } + write!(f, "/{}/{}/", term.keychain, term.index)?; + if tweaks.len() > 1 { + f.write_str("<")?; + } + let mut iter2 = tweaks.iter().peekable(); + while let Some(tweak) = iter2.next() { + write!(f, "{tweak}")?; + if iter2.peek().is_some() { + f.write_str(";")?; + } else if tweaks.len() > 1 { + f.write_str(">")?; + } + } + if iter1.peek().is_some() { + f.write_str(",")?; + } + } + f.write_str(")") + } +} + +#[cfg(test)] +mod test { + use core::str::FromStr; + + use bpstd::dbc::tapret::TapretCommitment; + use bpstd::seals::{TxoSealExt, WOutpoint, WTxoSeal}; + use bpstd::{Idx, Keychain, NormalIndex, Terminal, TrKey, Wpkh, XpubDerivable}; + use commit_verify::{Digest, Sha256}; + use rgb::{Outpoint, Txid}; + + use crate::RgbDescr; + + fn base_descr() -> RgbDescr { + let s = "[643a7adc/86'/1'/0']tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1>/*"; + let xpub = XpubDerivable::from_str(s).unwrap(); + RgbDescr::::new_unfunded(Wpkh::from(xpub), [0xADu8; 32]) + } + + #[test] + fn opret() { + let descr = base_descr(); + let s = descr.to_string(); + assert_eq!(s, "rgb(\ + wpkh([643a7adc/86h/1h/0h]tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1>/*),\ + seals(),\ + adadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadad\ + )"); + assert_eq!(RgbDescr::from_str(&s).unwrap(), descr); + } + + #[test] + fn tapret() { + let s = "[643a7adc/86'/1'/0']tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1>/*"; + let xpub = XpubDerivable::from_str(s).unwrap(); + let mut descr = RgbDescr::::new_unfunded(TrKey::from(xpub), [0xADu8; 32]); + let s = descr.to_string(); + assert_eq!(s, "rgb(\ + tapret(\ + tr([643a7adc/86h/1h/0h]tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1>/*),\ + tweaks()\ + ),\ + seals(),\ + adadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadad\ + )"); + assert_eq!(RgbDescr::from_str(&s).unwrap(), descr); + + descr.add_tweak( + Terminal::new(Keychain::OUTER, NormalIndex::ONE), + TapretCommitment::from([0xBAu8; 33]), + ); + let s = descr.to_string(); + assert_eq!(s, "rgb(\ + tapret(\ + tr([643a7adc/86h/1h/0h]tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1>/*),\ + tweaks(/0/1/xUGpuwjSUfFQ53BB6PCh36sjttPpYqXq6tNPXw2mC28mo)\ + ),\ + seals(),\ + adadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadad\ + )"); + assert_eq!(RgbDescr::from_str(&s).unwrap(), descr); + + descr.add_tweak( + Terminal::new(Keychain::INNER, NormalIndex::ZERO), + TapretCommitment::from([0xABu8; 33]), + ); + let s = descr.to_string(); + assert_eq!(s, "rgb(\ + tapret(\ + tr([643a7adc/86h/1h/0h]tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1>/*),\ + tweaks(\ + /0/1/xUGpuwjSUfFQ53BB6PCh36sjttPpYqXq6tNPXw2mC28mo,\ + /1/0/szpHvMPBKt4t9PagDS68oqS8dUc1gZTUPFV5p9Wgh4rF4\ + )\ + ),\ + seals(),\ + adadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadad\ + )"); + assert_eq!(RgbDescr::from_str(&s).unwrap(), descr); + + descr.add_tweak( + Terminal::new(Keychain::INNER, NormalIndex::ZERO), + TapretCommitment::from([0x43u8; 33]), + ); + let s = descr.to_string(); + assert_eq!(s, "rgb(\ + tapret(\ + tr([643a7adc/86h/1h/0h]tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1>/*),\ + tweaks(\ + /0/1/xUGpuwjSUfFQ53BB6PCh36sjttPpYqXq6tNPXw2mC28mo,\ + /1/0/\ + )\ + ),\ + seals(),\ + adadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadad\ + )"); + assert_eq!(RgbDescr::from_str(&s).unwrap(), descr); + } + + #[test] + fn base_seals() { + let mut descr = base_descr(); + + let sha = Sha256::new(); + descr.add_seal(WTxoSeal::no_fallback(Outpoint::new(Txid::from([0xDE; 32]), 129), sha, 56)); + let s = descr.to_string(); + assert_eq!(s, "rgb(\ + wpkh([643a7adc/86h/1h/0h]tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1>/*),\ + seals(dededededededededededededededededededededededededededededededede:129/F8GCQc9BuWAA7kGouPwxjZMA9WBNgFGFHG9kqYDNPFrN),\ + adadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadad\ + )"); + assert_eq!(RgbDescr::from_str(&s).unwrap(), descr); + + let sha = Sha256::new_with_prefix("test"); + descr.add_seal(WTxoSeal::no_fallback(Outpoint::new(Txid::from([0x13; 32]), 129), sha, 56)); + let s = descr.to_string(); + assert_eq!(s, "rgb(\ + wpkh([643a7adc/86h/1h/0h]tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1>/*),\ + seals(\ + 1313131313131313131313131313131313131313131313131313131313131313:129/397G2XyBYQZZX6YxnTHyJocEPszPQZTmnwBRQcpGuMCu,\ + dededededededededededededededededededededededededededededededede:129/F8GCQc9BuWAA7kGouPwxjZMA9WBNgFGFHG9kqYDNPFrN\ + ),\ + adadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadad\ + )"); + assert_eq!(RgbDescr::from_str(&s).unwrap(), descr); + } + + #[test] + fn fallback_seal() { + let mut descr = base_descr(); + + let seal = WTxoSeal { + primary: WOutpoint::Extern(Outpoint::new(Txid::from([0xDE; 32]), 129)), + secondary: TxoSealExt::Fallback(Outpoint::new(Txid::from([0xAF; 32]), 2)), + }; + descr.add_seal(seal); + let s = descr.to_string(); + assert_eq!(s, "rgb(\ + wpkh([643a7adc/86h/1h/0h]tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1>/*),\ + seals(dededededededededededededededededededededededededededededededede:129/afafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafaf:2),\ + adadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadad\ + )"); + assert_eq!(RgbDescr::from_str(&s).unwrap(), descr); + } +} diff --git a/descriptors/src/lib.rs b/descriptors/src/lib.rs index 97f6df7..5c8a75b 100644 --- a/descriptors/src/lib.rs +++ b/descriptors/src/lib.rs @@ -33,8 +33,10 @@ extern crate amplify; extern crate serde; extern crate core; +mod display; +mod parse; + use alloc::collections::{BTreeMap, BTreeSet}; -use core::fmt::{self, Display, Formatter}; use amplify::{Bytes32, Wrapper, WrapperMut}; use bpstd::dbc::tapret::TapretCommitment; @@ -48,6 +50,8 @@ use bpstd::{ use commit_verify::CommitVerify; use indexmap::IndexMap; +pub use self::parse::{RgbDescrParseError, SealParseError, TweakParseError}; + pub trait DescriptorRgb: Descriptor { fn add_seal(&self, seal: WTxoSeal); } @@ -58,48 +62,13 @@ pub trait DescriptorRgb: Descriptor { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))] pub struct SealDescr(BTreeSet); -impl Display for SealDescr { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_str("seals(")?; - let mut iter = self.0.iter().peekable(); - while let Some(seal) = iter.next() { - Display::fmt(seal, f)?; - if iter.peek().is_some() { - f.write_str(",")?; - } - } - f.write_str(")") - } -} - #[derive(Wrapper, WrapperMut, Clone, PartialEq, Eq, Debug, Default, From)] #[wrapper(Deref)] #[wrapper_mut(DerefMut)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))] -pub struct TapretWeaks(BTreeMap>); - -impl Display for TapretWeaks { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_str("tweaks(")?; - let mut iter1 = self.iter().peekable(); - while let Some((term, tweaks)) = iter1.next() { - write!(f, "{term}=")?; - let mut iter2 = tweaks.iter().peekable(); - while let Some(tweak) = iter2.next() { - write!(f, "{tweak}")?; - if iter2.peek().is_some() { - f.write_str(",")?; - } - } - if iter1.peek().is_some() { - f.write_str(";")?; - } - } - f.write_str(")") - } -} +pub struct TapretTweaks(BTreeMap>); -#[derive(Clone, Debug, Display, From)] +#[derive(Clone, PartialEq, Eq, Debug, Display, From)] #[cfg_attr( feature = "serde", derive(Serialize, Deserialize), @@ -118,10 +87,10 @@ enum RgbDeriver { #[display(inner)] OpretOnly(StdDescr), - #[display("{tr},{tweaks}")] + #[display("tapret({tr},{tweaks})")] Universal { tr: Tr, - tweaks: TapretWeaks, + tweaks: TapretTweaks, }, } @@ -233,8 +202,8 @@ impl + DeriveLegacy + DeriveCompr } } -#[derive(Clone, Debug, Display)] -#[display("rgb({deriver},{seals},noise({noise:x}))")] +#[derive(Clone, PartialEq, Eq, Debug, Display)] +#[display("rgb({deriver},{seals},{noise:x})")] #[cfg_attr( feature = "serde", derive(Serialize, Deserialize), @@ -368,7 +337,7 @@ mod test { #[test] fn descr_serde() { - let s = "[643a7adc/86'/1'/0']tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1;9;10>/*"; + let s = "[643a7adc/86'/1'/0']tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1>/*"; let xpub = XpubDerivable::from_str(s).unwrap(); let descr = RgbDescr::::new_unfunded(Wpkh::from(xpub), [0u8; 32]); @@ -383,7 +352,7 @@ noise = "0000000000000000000000000000000000000000000000000000000000000000" nonce = 0 [deriver.opretOnly] -wpkh = "[643a7adc/86h/1h/0h]tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1;9;10>/*" +wpkh = "[643a7adc/86h/1h/0h]tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1>/*" "# ); } diff --git a/descriptors/src/parse.rs b/descriptors/src/parse.rs new file mode 100644 index 0000000..1824e6d --- /dev/null +++ b/descriptors/src/parse.rs @@ -0,0 +1,345 @@ +// Wallet Library for RGB smart contracts +// +// SPDX-License-Identifier: Apache-2.0 +// +// Designed in 2019-2025 by Dr Maxim Orlovsky +// Written in 2024-2025 by Dr Maxim Orlovsky +// +// Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. +// Copyright (C) 2024-2025 LNP/BP Laboratories, +// Institute for Distributed and Cognitive Systems (InDCS), Switzerland. +// Copyright (C) 2025 RGB Consortium, Switzerland. +// Copyright (C) 2019-2025 Dr Maxim Orlovsky. +// All rights under the above copyrights are reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. + +use alloc::collections::BTreeSet; +use core::error::Error; +use core::fmt::Display; +use core::num::ParseIntError; +use core::str::FromStr; + +use amplify::{hex, Bytes32}; +use bpstd::compiler::{check_forms, DescrAst, DescrExpr, DescrParseError, NoKey, ScriptExpr}; +use bpstd::dbc::tapret::TapretCommitment; +use bpstd::seals::{Noise, TxoSealExt, WOutpoint, WTxoSeal}; +use bpstd::{ + DeriveSet, IndexParseError, Keychain, NormalIndex, Outpoint, OutpointParseError, StdDescr, + Terminal, Tr, Vout, XkeyDecodeError, XkeyParseError, +}; + +use crate::{RgbDeriver, RgbDescr, SealDescr, TapretTweaks}; + +#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] +#[display(doc_comments)] +pub enum SealParseError { + /// invalid seal literal '{0}' in position {1}. + InvalidSeal(String, usize), + + /// failback information is absent from a seal definition in '{0}'. + NoFailback(String), + + /// invalid primary outpoint in seal definition string. {0} + InvalidPrimary(OutpointParseError), + + /// invalid fallback outpoint in seal definition string. {0} + InvalidFallback(OutpointParseError), + + /// invalid noise data in seal definition. {0} + InvalidNoise(String), + + #[from] + /// invalid transaction output number in seal definition. {0} + InvalidVout(ParseIntError), +} + +impl From for SealParseError { + fn from(_: XkeyDecodeError) -> Self { unreachable!() } +} + +impl FromStr for SealDescr { + type Err = DescrParseError; + + fn from_str(s: &str) -> Result { + let ast = ScriptExpr::::from_str(s).map_err(DescrParseError::from)?; + Self::parse_ast(ast) + } +} + +impl SealDescr { + pub fn parse_ast( + ast: ScriptExpr, + ) -> Result> + where K::Err: Error { + if ast.name == "seals" && ast.children.is_empty() { + return Ok(SealDescr::default()); + } + let form = check_forms(ast, "seals", &[DescrExpr::VariadicLit][..]) + .ok_or(DescrParseError::InvalidArgs("seals"))?; + let mut set = bset![]; + for item in form { + let DescrAst::Lit(s, _) = item else { + unreachable!(); + }; + let (prim, sec) = s + .split_once('/') + .ok_or_else(|| SealParseError::NoFailback(s.to_owned())) + .map_err(|e| DescrParseError::Expr("seal", e))?; + let primary = if let Some(vout) = prim.strip_prefix("~:") { + WOutpoint::Wout(Vout::from_str(vout)?) + } else { + WOutpoint::Extern( + Outpoint::from_str(prim) + .map_err(SealParseError::InvalidPrimary) + .map_err(|e| DescrParseError::Expr("seal", e))?, + ) + }; + let secondary = if sec.contains(':') { + let fallback = Outpoint::from_str(sec) + .map_err(SealParseError::InvalidFallback) + .map_err(|e| DescrParseError::Expr("seal", e))?; + TxoSealExt::Fallback(fallback) + } else { + TxoSealExt::Noise( + Noise::from_str(sec) + .map_err(|_| SealParseError::InvalidNoise(sec.to_owned())) + .map_err(|e| DescrParseError::Expr("seal", e))?, + ) + }; + let seal = WTxoSeal { primary, secondary }; + set.insert(seal); + } + Ok(SealDescr::from(set)) + } +} + +#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] +#[display(doc_comments)] +pub enum TweakParseError { + /// invalid structure of tapret tweak expression '{0}'. + InvalidStructure(String), + + #[from] + /// invalid format of derivation index. {0} + InvalidIndex(IndexParseError), + + /// invalid tapret tweak value '{0}'. + InvalidTweak(String), +} + +impl From for TweakParseError { + fn from(_: XkeyDecodeError) -> Self { unreachable!() } +} + +impl FromStr for TapretTweaks { + type Err = DescrParseError; + + fn from_str(s: &str) -> Result { + let ast = ScriptExpr::::from_str(s).map_err(DescrParseError::from)?; + Self::parse_ast(ast) + } +} + +impl TapretTweaks { + pub fn parse_ast( + ast: ScriptExpr, + ) -> Result> + where K::Err: Error { + if ast.name == "tapret" && ast.children.is_empty() { + return Ok(TapretTweaks::default()); + } + let form = check_forms(ast, "tweaks", &[DescrExpr::VariadicLit][..]) + .ok_or(DescrParseError::InvalidArgs("tweaks"))?; + let mut map = bmap! {}; + for item in form { + let DescrAst::Lit(s, _) = item else { + unreachable!(); + }; + let mut split = s.split('/'); + if split.next() != Some("") { + return Err(DescrParseError::Expr( + "tapret tweak", + TweakParseError::InvalidStructure(s.to_owned()), + )); + } + let keychain = split + .next() + .ok_or_else(|| TweakParseError::InvalidStructure(s.to_owned())) + .map_err(|e| DescrParseError::Expr("tapret tweak", e))? + .parse::()?; + let index = split + .next() + .ok_or_else(|| TweakParseError::InvalidStructure(s.to_owned())) + .map_err(|e| DescrParseError::Expr("tapret tweak", e))? + .parse::() + .map_err(|e| DescrParseError::Expr("tapret tweak", e.into()))?; + let Some(rest) = split.next() else { + continue; + }; + let term = Terminal::new(keychain, index); + let set: &mut BTreeSet = map.entry(term).or_default(); + if let Some(s) = rest.strip_prefix("<").and_then(|s| s.strip_suffix(">")) { + for tweak in s.split(';') { + let tweak = TapretCommitment::from_str(tweak).map_err(|_| { + DescrParseError::Expr( + "tapret tweak", + TweakParseError::InvalidTweak(tweak.to_owned()), + ) + })?; + set.insert(tweak); + } + } else { + let tweak = TapretCommitment::from_str(rest).map_err(|_| { + DescrParseError::Expr( + "tapret tweak", + TweakParseError::InvalidTweak(rest.to_owned()), + ) + })?; + + set.insert(tweak); + } + } + Ok(TapretTweaks::from(map)) + } +} + +#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] +#[display(inner)] +pub enum RgbDescrParseError { + #[from] + Descr(DescrParseError), + + #[from] + Tweak(TweakParseError), + + #[from] + Seal(SealParseError), + + #[from] + Noise(hex::Error), +} + +impl From for XkeyParseError { + fn from(_: TweakParseError) -> Self { + panic!("TweakParseError cannot be converted to XkeyParseError") + } +} + +impl From for XkeyParseError { + fn from(_: SealParseError) -> Self { + panic!("TweakParseError cannot be converted to SealParseError") + } +} + +impl RgbDescrParseError { + pub fn from_tweak_err(err: DescrParseError) -> Self + where E: From { + match err { + DescrParseError::Expr(_, err) => Self::Tweak(err), + err => Self::Descr(DescrParseError::from(err)), + } + } + + pub fn from_seal_err(err: DescrParseError) -> Self + where E: From { + match err { + DescrParseError::Expr(_, err) => Self::Seal(err), + err => Self::Descr(DescrParseError::from(err)), + } + } +} + +impl FromStr for RgbDeriver +where + K::Err: Error + From, + K::Legacy: Display + FromStr, + K::Compr: Display + FromStr, + K::XOnly: Display + FromStr, +{ + type Err = RgbDescrParseError; + + fn from_str(s: &str) -> Result { + if s.trim_start().starts_with("tapret") { + let ast = ScriptExpr::::from_str(s).map_err(DescrParseError::from)?; + Self::parse_ast(ast) + } else { + StdDescr::from_str(s) + .map(Self::OpretOnly) + .map_err(RgbDescrParseError::Descr) + } + } +} + +impl RgbDeriver +where + K::Err: Error + From, + K::Legacy: Display + FromStr, + K::Compr: Display + FromStr, + K::XOnly: Display + FromStr, +{ + pub fn parse_ast(ast: ScriptExpr) -> Result> { + if ast.name == "tapret" { + let mut form = check_forms(ast, "tapret", &[DescrExpr::Script, DescrExpr::Script][..]) + .ok_or(DescrParseError::InvalidArgs("tapret"))?; + + let Some(DescrAst::Script(tweaks)) = form.pop() else { + unreachable!(); + }; + let Some(DescrAst::Script(tr)) = form.pop() else { + unreachable!(); + }; + + let tweaks = + TapretTweaks::parse_ast(*tweaks).map_err(RgbDescrParseError::from_tweak_err)?; + let tr = Tr::from_str(tr.full)?; + + Ok(Self::Universal { tr, tweaks }) + } else { + StdDescr::from_str(ast.full) + .map(Self::OpretOnly) + .map_err(RgbDescrParseError::Descr) + } + } +} + +impl FromStr for RgbDescr +where + K::Err: Error + From + From, + K::Legacy: Display + FromStr, + K::Compr: Display + FromStr, + K::XOnly: Display + FromStr, +{ + type Err = RgbDescrParseError; + + fn from_str(s: &str) -> Result { + let ast = ScriptExpr::::from_str(s).map_err(DescrParseError::from)?; + let mut form = + check_forms(ast, "rgb", &[DescrExpr::Script, DescrExpr::Script, DescrExpr::Lit][..]) + .ok_or(DescrParseError::InvalidArgs("rgb"))?; + + let Some(DescrAst::Lit(noise, _)) = form.pop() else { + unreachable!(); + }; + let Some(DescrAst::Script(seals)) = form.pop() else { + unreachable!(); + }; + let Some(DescrAst::Script(descr)) = form.pop() else { + unreachable!(); + }; + + let deriver = RgbDeriver::parse_ast(*descr)?; + let seals = SealDescr::parse_ast(*seals).map_err(RgbDescrParseError::from_seal_err)?; + let noise = Bytes32::from_str(noise)?; + + Ok(Self { deriver, seals, noise, nonce: 0 }) + } +}