diff --git a/.gitignore b/.gitignore index f9fa99e1..ca46687c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target *.sqlite +*.DS_Store diff --git a/Cargo.lock b/Cargo.lock index b12ab6e7..8d6655fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.16" @@ -38,18 +47,15 @@ checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "arbitrary" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" -dependencies = [ - "derive_arbitrary", -] [[package]] name = "asn1-rs" @@ -258,16 +264,16 @@ dependencies = [ [[package]] name = "chia-bls" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82db17433a5830b55f678d4d143fec4f04668e5909c7be3468f2b3067673308c" +checksum = "a8df881bc212d42e043dd4e0c123e2c22f7627d4ed1ffb4d1b63f405b5d1acaa" dependencies = [ "anyhow", - "arbitrary", "blst", - "chia-traits 0.7.0", + "chia-traits 0.8.0", "hex", "hkdf", + "lru", "sha2 0.10.8", "thiserror", "tiny-bip39", @@ -275,12 +281,12 @@ dependencies = [ [[package]] name = "chia-client" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e63b55cf5f8dd9b4943541c96ed9355e6ae23d3c5cf064bc3a1f13417bd09f" +checksum = "34b6b73380e3fc49b8fdcb98ba4b55c82633b99d65811b9472a07eff19a7ae69" dependencies = [ "chia-protocol", - "chia-traits 0.7.0", + "chia-traits 0.8.0", "futures-util", "thiserror", "tokio", @@ -290,14 +296,15 @@ dependencies = [ [[package]] name = "chia-consensus" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "971cd78633bb54fa52ecf98aaa56d09027ce17189dd65a1f99b1057ddc148f7d" +checksum = "d3fa2616765266a85960196cde6abd537fcc8efe5e8e6c5655f225f7143cfa2f" dependencies = [ + "chia-bls 0.8.0", "chia-protocol", - "chia-traits 0.7.0", - "chia-wallet", - "chia_streamable_macro 0.6.0", + "chia-puzzles", + "chia-traits 0.8.0", + "chia_streamable_macro 0.8.0", "clvm-derive", "clvm-traits", "clvm-utils", @@ -310,14 +317,13 @@ dependencies = [ [[package]] name = "chia-protocol" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c97afc52ecb2f0f29bee3ba13d2af53d80309af4e6c8e858ef70131bc375dd2" +checksum = "a4024a0d4955781983962ec06040443985fb86a7b969d7287bd05c2041259b5f" dependencies = [ - "arbitrary", - "chia-bls 0.7.0", - "chia-traits 0.7.0", - "chia_streamable_macro 0.6.0", + "chia-bls 0.8.0", + "chia-traits 0.8.0", + "chia_streamable_macro 0.8.0", "clvm-traits", "clvm-utils", "clvmr", @@ -325,6 +331,22 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "chia-puzzles" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a6e1ac8bd718b60e0b95d80301b31bb3894dc688c6ca77f4996bfa542b3ac4b" +dependencies = [ + "chia-bls 0.8.0", + "chia-protocol", + "clvm-traits", + "clvm-utils", + "clvmr", + "hex-literal", + "num-bigint", + "sha2 0.10.8", +] + [[package]] name = "chia-ssl" version = "0.7.0" @@ -353,54 +375,43 @@ dependencies = [ [[package]] name = "chia-traits" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58c4b856bcd141893b3b21b921764c6d76d4c73f20924bc11bdc3d1df10945a" +checksum = "2bca924e54d99288562e7c7e2f7dc39e12a0a1ee772b44a07ed5248199e8ac3f" dependencies = [ - "chia_streamable_macro 0.6.0", - "hex", + "chia_streamable_macro 0.8.0", "sha2 0.10.8", "thiserror", ] -[[package]] -name = "chia-wallet" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e9c5d92b703e8c4457ccfd89529403f888d6d8d700af35244fda0e71c4de8f" -dependencies = [ - "arbitrary", - "chia-bls 0.7.0", - "chia-protocol", - "clvm-traits", - "clvm-utils", - "clvmr", - "hex-literal", - "num-bigint", - "sha2 0.10.8", -] - [[package]] name = "chia-wallet-sdk" version = "0.7.1" dependencies = [ + "anyhow", "bech32", "bip39", - "chia-bls 0.7.0", + "chia-bls 0.8.0", "chia-client", "chia-consensus", "chia-protocol", + "chia-puzzles", "chia-ssl", - "chia-wallet", + "chia-traits 0.8.0", "clvm-traits", "clvm-utils", "clvmr", + "flate2", + "futures-channel", + "futures-util", "hex", "hex-literal", + "indexmap", "native-tls", "once_cell", "rand", "rand_chacha", + "rstest", "serde", "serde_json", "sha2 0.9.9", @@ -424,9 +435,9 @@ dependencies = [ [[package]] name = "chia_streamable_macro" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68fa14fa30a2560f36afb0d9f88d33a5d630858e0f893dc5e7094f06063d9e86" +checksum = "a1e2025e62defea0c0bb0912e15091e632cdad892986563e179ce9ad3d2c7d0b" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -447,11 +458,11 @@ dependencies = [ [[package]] name = "clvm-traits" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6fad36c3bffe918b28a0f56b00ada0b898e8932a4b08206596ebb8a79ec8d15" +checksum = "25b0460979b59433e5163ce90c95e6ad7d4448b0b01bf8a186f280320bec770e" dependencies = [ - "chia-bls 0.7.0", + "chia-bls 0.8.0", "clvm-derive", "clvmr", "num-bigint", @@ -460,19 +471,20 @@ dependencies = [ [[package]] name = "clvm-utils" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf3ba043f6bc8101b4ad5767e3701decdc722a66cc16caefba8b8a44f09dece" +checksum = "59ba275ebb00b110b331fdf326ebf1c223ec092eb3468df08a8a3c16b6495746" dependencies = [ "clvm-traits", "clvmr", + "hex", ] [[package]] name = "clvmr" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb235bb5ce94e1e64e54cede97ca5a4766590d3584dea1b4285499d8e7f56f6" +checksum = "b4b05784a842671ccedbefb0e3dabab7d0eb3411395f5182a4c68722a5284591" dependencies = [ "chia-bls 0.4.0", "hex-literal", @@ -531,6 +543,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-queue" version = "0.3.11" @@ -608,17 +629,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive_arbitrary" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] - [[package]] name = "digest" version = "0.9.0" @@ -755,6 +765,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" +[[package]] +name = "flate2" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7" +dependencies = [ + "crc32fast", + "libz-sys", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.11.0" @@ -796,6 +817,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -863,12 +899,19 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1027,9 +1070,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.2" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", @@ -1105,6 +1148,17 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-sys" +version = "1.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -1127,6 +1181,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown", +] + [[package]] name = "md-5" version = "0.10.6" @@ -1561,6 +1624,41 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "relative-path" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" + [[package]] name = "rfc6979" version = "0.4.0" @@ -1606,6 +1704,35 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rstest" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.48", + "unicode-ident", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1618,6 +1745,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rusticata-macros" version = "4.1.0" @@ -1698,6 +1834,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + [[package]] name = "serde" version = "1.0.197" diff --git a/Cargo.toml b/Cargo.toml index e222c23f..4fc1bca7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,14 +15,16 @@ sqlite = ["dep:sqlx"] tokio = { version = "1.33.0", features = ["macros"] } sha2 = "0.9.9" thiserror = "1.0.50" -clvmr = "0.6.1" -chia-protocol = "0.7.0" -chia-wallet = "0.7.0" -chia-bls = "0.7.0" -chia-client = "0.7.0" -clvm-traits = "0.7.0" -clvm-utils = "0.7.0" +clvmr = "0.7.0" +chia-protocol = "0.8.0" +chia-puzzles = "0.8.0" +chia-bls = "0.8.0" +chia-client = "0.8.0" +clvm-traits = "0.8.0" +clvm-utils = "0.8.0" chia-ssl = "0.7.0" +chia-traits = "0.8.0" +chia-consensus = "0.8.0" native-tls = "0.2.11" tokio-tungstenite = { version = "0.21.0", features = ["native-tls"] } hex = "0.4.3" @@ -33,10 +35,15 @@ sqlx = { version = "0.7.4", features = ["runtime-tokio", "sqlite"], optional = t hex-literal = "0.4.1" serde_json = "1.0.115" serde = { version = "1.0.197", features = ["derive"] } +futures-util = "0.3.30" +futures-channel = { version = "0.3.30", features = ["sink"] } +indexmap = "2.2.6" +flate2 = { version = "1.0.29", features = ["zlib"] } [dev-dependencies] +anyhow = "1.0.82" bip39 = "2.0.0" -chia-consensus = "0.7.0" once_cell = "1.19.0" +rstest = "0.19.0" sqlx = { version = "0.7.4", features = ["runtime-tokio", "sqlite"] } tokio = { version = "1.33.0", features = ["full"] } diff --git a/examples/key_stores.rs b/examples/key_stores.rs index 2833c756..204af44e 100644 --- a/examples/key_stores.rs +++ b/examples/key_stores.rs @@ -7,7 +7,7 @@ use chia_bls::{ }, DerivableKey, PublicKey, SecretKey, }; -use chia_wallet::{standard::DEFAULT_HIDDEN_PUZZLE_HASH, DeriveSynthetic}; +use chia_puzzles::DeriveSynthetic; use chia_wallet_sdk::sqlite::{fetch_puzzle_hash, insert_keys, SQLITE_MIGRATOR}; use sqlx::SqlitePool; @@ -32,11 +32,7 @@ async fn main() -> Result<(), Box> { let int_sk = master_to_wallet_hardened_intermediate(&root_sk); let unhardened_pks: Vec = (0..100) - .map(|index| { - int_pk - .derive_unhardened(index) - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH) - }) + .map(|index| int_pk.derive_unhardened(index).derive_synthetic()) .collect(); insert_keys(&mut tx, 0, unhardened_pks.as_slice(), false).await?; @@ -45,7 +41,7 @@ async fn main() -> Result<(), Box> { int_sk .derive_hardened(index) .public_key() - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH) + .derive_synthetic() }) .collect(); insert_keys(&mut tx, 0, hardened_pks.as_slice(), true).await?; diff --git a/src/address.rs b/src/address.rs index a1c4a7e4..7156f7e9 100644 --- a/src/address.rs +++ b/src/address.rs @@ -5,10 +5,6 @@ use thiserror::Error; /// Errors you can get while trying to decode an address. #[derive(Error, Debug, Clone, PartialEq, Eq)] pub enum AddressError { - /// The wrong HRP prefix was used. - #[error("invalid prefix `{0}`")] - InvalidPrefix(String), - /// The address was encoded as bech32, rather than bech32m. #[error("encoding is not bech32m")] InvalidFormat, @@ -54,6 +50,7 @@ pub fn encode_puzzle_hash(puzzle_hash: [u8; 32], include_0x: bool) -> String { /// Decodes an address into a puzzle hash and HRP prefix. pub fn decode_address(address: &str) -> Result<([u8; 32], String), AddressError> { let (hrp, data, variant) = bech32::decode(address)?; + if variant != Variant::Bech32m { return Err(AddressError::InvalidFormat); } diff --git a/src/condition.rs b/src/condition.rs index 5ac64c0c..ee421443 100644 --- a/src/condition.rs +++ b/src/condition.rs @@ -104,6 +104,22 @@ pub enum CreateCoin { WithMemos(CreateCoinWithMemos), } +impl CreateCoin { + pub fn puzzle_hash(&self) -> Bytes32 { + match self { + Self::WithoutMemos(inner) => inner.puzzle_hash, + Self::WithMemos(inner) => inner.puzzle_hash, + } + } + + pub fn amount(&self) -> u64 { + match self { + Self::WithoutMemos(inner) => inner.amount, + Self::WithMemos(inner) => inner.amount, + } + } +} + condition!(Remark, 1, {}); condition!(AggSigParent, 43, { public_key: PublicKey, message: Bytes }); condition!(AggSigPuzzle, 44, { public_key: PublicKey, message: Bytes }); @@ -117,9 +133,9 @@ condition!(CreateCoinWithoutMemos, 51, { puzzle_hash: Bytes32, amount: u64 }); condition!(CreateCoinWithMemos, 51, { puzzle_hash: Bytes32, amount: u64, memos: Vec }); condition!(ReserveFee, 52, { amount: u64 }); condition!(CreateCoinAnnouncement, 60, { message: Bytes }); -condition!(AssertCoinAnnouncement, 61, { announcement_id: Bytes }); +condition!(AssertCoinAnnouncement, 61, { announcement_id: Bytes32 }); condition!(CreatePuzzleAnnouncement, 62, { message: Bytes }); -condition!(AssertPuzzleAnnouncement, 63, { announcement_id: Bytes }); +condition!(AssertPuzzleAnnouncement, 63, { announcement_id: Bytes32 }); condition!(AssertConcurrentSpend, 64, { coin_id: Bytes32 }); condition!(AssertConcurrentPuzzle, 65, { puzzle_hash: Bytes32 }); condition!(AssertMyCoinId, 70, { coin_id: Bytes32 }); diff --git a/src/lib.rs b/src/lib.rs index 495cc4f8..e8acd00f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,8 @@ -#![deny(missing_docs)] #![doc = include_str!("../README.md")] mod address; mod condition; +mod parser; mod spends; mod ssl; @@ -18,11 +18,13 @@ pub mod wallet; pub use address::*; pub use condition::*; +pub use parser::*; pub use spends::*; pub use ssl::*; pub use wallet::*; -fn trim_leading_zeros(mut slice: &[u8]) -> &[u8] { +/// Removes the leading zeros from a CLVM atom. +pub fn trim_leading_zeros(mut slice: &[u8]) -> &[u8] { while (!slice.is_empty()) && (slice[0] == 0) { if slice.len() > 1 && (slice[1] & 0x80 == 0x80) { break; @@ -32,6 +34,24 @@ fn trim_leading_zeros(mut slice: &[u8]) -> &[u8] { slice } +/// Converts a `usize` to an atom in CLVM format, with leading zeros trimmed. +pub fn usize_to_bytes(num: usize) -> Vec { + let bytes: Vec = num.to_be_bytes().into(); + trim_leading_zeros(bytes.as_slice()).to_vec() +} + +/// Converts a `u64` to an atom in CLVM format, with leading zeros trimmed. +pub fn u64_to_bytes(num: u64) -> Vec { + let bytes: Vec = num.to_be_bytes().into(); + trim_leading_zeros(bytes.as_slice()).to_vec() +} + +/// Converts a `u16` to an atom in CLVM format, with leading zeros trimmed. +pub fn u16_to_bytes(num: u16) -> Vec { + let bytes: Vec = num.to_be_bytes().into(); + trim_leading_zeros(bytes.as_slice()).to_vec() +} + #[cfg(test)] mod testing { use std::str::FromStr; diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 00000000..40172dca --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,7 @@ +mod parse_context; +mod parse_error; +mod puzzles; + +pub use parse_context::*; +pub use parse_error::*; +pub use puzzles::*; diff --git a/src/parser/parse_context.rs b/src/parser/parse_context.rs new file mode 100644 index 00000000..b03fbc17 --- /dev/null +++ b/src/parser/parse_context.rs @@ -0,0 +1,54 @@ +use chia_protocol::{Bytes32, Coin}; +use clvm_traits::FromClvm; +use clvm_utils::{tree_hash, CurriedProgram}; +use clvmr::{Allocator, NodePtr}; + +use crate::ParseError; + +pub struct ParseContext { + mod_hash: Bytes32, + args: NodePtr, + solution: NodePtr, + parent_coin: Coin, + coin: Coin, +} + +impl ParseContext { + pub fn mod_hash(&self) -> Bytes32 { + self.mod_hash + } + + pub fn args(&self) -> NodePtr { + self.args + } + + pub fn solution(&self) -> NodePtr { + self.solution + } + + pub fn parent_coin(&self) -> Coin { + self.parent_coin + } + + pub fn coin(&self) -> Coin { + self.coin + } +} + +pub fn parse_puzzle( + allocator: &mut Allocator, + parent_puzzle: NodePtr, + parent_solution: NodePtr, + parent_coin: Coin, + coin: Coin, +) -> Result { + let CurriedProgram { program, args } = CurriedProgram::from_clvm(allocator, parent_puzzle)?; + + Ok(ParseContext { + mod_hash: tree_hash(allocator, program).into(), + args, + solution: parent_solution, + parent_coin, + coin, + }) +} diff --git a/src/parser/parse_error.rs b/src/parser/parse_error.rs new file mode 100644 index 00000000..cffa5e32 --- /dev/null +++ b/src/parser/parse_error.rs @@ -0,0 +1,27 @@ +use clvm_traits::FromClvmError; +use clvmr::reduction::EvalErr; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ParseError { + #[error("Eval error: {0}")] + Eval(#[from] EvalErr), + + #[error("CLVM error: {0}")] + FromClvm(#[from] FromClvmError), + + #[error("Invalid puzzle")] + InvalidPuzzle, + + #[error("Incorrect hint")] + MissingCreateCoin, + + #[error("DID singleton struct mismatch")] + DidSingletonStructMismatch, + + #[error("Invalid singleton struct")] + InvalidSingletonStruct, + + #[error("Unknown DID output")] + UnknownDidOutput, +} diff --git a/src/parser/puzzles.rs b/src/parser/puzzles.rs new file mode 100644 index 00000000..b6dd4911 --- /dev/null +++ b/src/parser/puzzles.rs @@ -0,0 +1,7 @@ +mod cat; +mod did; +mod singleton; + +pub use cat::*; +pub use did::*; +pub use singleton::*; diff --git a/src/parser/puzzles/cat.rs b/src/parser/puzzles/cat.rs new file mode 100644 index 00000000..5206ea51 --- /dev/null +++ b/src/parser/puzzles/cat.rs @@ -0,0 +1,158 @@ +use chia_protocol::Bytes32; +use chia_puzzles::{ + cat::{CatArgs, CatSolution, CAT_PUZZLE_HASH}, + LineageProof, +}; +use clvm_traits::FromClvm; +use clvm_utils::{tree_hash, CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{reduction::Reduction, run_program, Allocator, ChiaDialect, NodePtr}; + +use crate::{CatInfo, CreateCoin, ParseContext, ParseError}; + +pub fn parse_cat( + allocator: &mut Allocator, + ctx: &ParseContext, + max_cost: u64, +) -> Result, ParseError> { + if ctx.mod_hash().to_bytes() != CAT_PUZZLE_HASH.to_bytes() { + return Ok(None); + } + + let args = CatArgs::::from_clvm(allocator, ctx.args())?; + let solution = CatSolution::::from_clvm(allocator, ctx.solution())?; + + let Reduction(_cost, output) = run_program( + allocator, + &ChiaDialect::new(0), + args.inner_puzzle, + solution.inner_puzzle_solution, + max_cost, + )?; + + let conditions = Vec::::from_clvm(allocator, output)?; + let mut p2_puzzle_hash = None; + + for condition in conditions { + let Ok(create_coin) = CreateCoin::from_clvm(allocator, condition) else { + continue; + }; + + let cat_puzzle_hash = CurriedProgram { + program: CAT_PUZZLE_HASH, + args: CatArgs { + mod_hash: CAT_PUZZLE_HASH.into(), + tail_program_hash: args.tail_program_hash, + inner_puzzle: TreeHash::from(create_coin.puzzle_hash()), + }, + } + .tree_hash(); + + if Bytes32::from(cat_puzzle_hash) == ctx.coin().puzzle_hash + && create_coin.amount() == ctx.coin().amount + { + p2_puzzle_hash = Some(create_coin.puzzle_hash()); + break; + } + } + + let Some(p2_puzzle_hash) = p2_puzzle_hash else { + return Err(ParseError::MissingCreateCoin); + }; + + Ok(Some(CatInfo { + asset_id: args.tail_program_hash, + p2_puzzle_hash, + coin: ctx.coin(), + lineage_proof: LineageProof { + parent_parent_coin_id: ctx.parent_coin().parent_coin_info, + parent_inner_puzzle_hash: tree_hash(allocator, args.inner_puzzle).into(), + parent_amount: ctx.parent_coin().amount, + }, + })) +} + +#[cfg(test)] +mod tests { + use chia_bls::PublicKey; + use chia_protocol::Coin; + use chia_puzzles::standard::{StandardArgs, STANDARD_PUZZLE_HASH}; + use clvm_traits::ToNodePtr; + use clvm_utils::CurriedProgram; + + use crate::{ + parse_puzzle, Chainable, CreateCoinWithMemos, IssueCat, SpendContext, StandardSpend, + }; + + use super::*; + + #[test] + fn test_parse_cat() -> anyhow::Result<()> { + let mut allocator = Allocator::new(); + let mut ctx = SpendContext::new(&mut allocator); + + let pk = PublicKey::default(); + let puzzle_hash = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { synthetic_key: pk }, + } + .tree_hash() + .into(); + let parent = Coin::new(Bytes32::default(), puzzle_hash, 1); + + let (issue_cat, issuance_info) = IssueCat::new(parent.coin_id()) + .condition(ctx.alloc(CreateCoinWithMemos { + puzzle_hash, + amount: 1, + memos: vec![puzzle_hash.to_vec().into()], + })?) + .multi_issuance(&mut ctx, pk, 1)?; + + let cat_puzzle_hash = CurriedProgram { + program: CAT_PUZZLE_HASH, + args: CatArgs { + mod_hash: CAT_PUZZLE_HASH.into(), + tail_program_hash: issuance_info.asset_id, + inner_puzzle: TreeHash::from(puzzle_hash), + }, + } + .tree_hash(); + + let cat_info = CatInfo { + asset_id: issuance_info.asset_id, + p2_puzzle_hash: puzzle_hash, + coin: Coin::new(issuance_info.eve_coin.coin_id(), cat_puzzle_hash.into(), 1), + lineage_proof: LineageProof { + parent_parent_coin_id: issuance_info.eve_coin.parent_coin_info, + parent_inner_puzzle_hash: issuance_info.eve_inner_puzzle_hash, + parent_amount: 1, + }, + }; + + StandardSpend::new() + .chain(issue_cat) + .finish(&mut ctx, parent, pk)?; + + let coin_spends = ctx.take_spends(); + + let coin_spend = coin_spends + .into_iter() + .find(|cs| cs.coin.coin_id() == issuance_info.eve_coin.coin_id()) + .unwrap(); + + let puzzle = coin_spend.puzzle_reveal.to_node_ptr(&mut allocator)?; + let solution = coin_spend.solution.to_node_ptr(&mut allocator)?; + + let parse_ctx = parse_puzzle( + &mut allocator, + puzzle, + solution, + coin_spend.coin, + cat_info.coin, + )?; + + let parse = parse_cat(&mut allocator, &parse_ctx, u64::MAX)?; + assert_eq!(parse, Some(cat_info)); + + Ok(()) + } +} diff --git a/src/parser/puzzles/did.rs b/src/parser/puzzles/did.rs new file mode 100644 index 00000000..a2915c19 --- /dev/null +++ b/src/parser/puzzles/did.rs @@ -0,0 +1,159 @@ +use chia_protocol::Bytes32; +use chia_puzzles::{ + did::{DidArgs, DidSolution, DID_INNER_PUZZLE_HASH}, + LineageProof, Proof, +}; +use clvm_traits::FromClvm; +use clvm_utils::tree_hash; +use clvmr::{reduction::Reduction, run_program, Allocator, ChiaDialect, NodePtr}; + +use crate::{ + did_inner_puzzle_hash, singleton_puzzle_hash, CreateCoinWithMemos, DidInfo, ParseContext, + ParseError, ParseSingleton, +}; + +pub fn parse_did( + allocator: &mut Allocator, + ctx: &ParseContext, + singleton: &ParseSingleton, + max_cost: u64, +) -> Result>, ParseError> { + if singleton.inner_mod_hash().to_bytes() != DID_INNER_PUZZLE_HASH.to_bytes() { + return Ok(None); + } + + let args = DidArgs::::from_clvm(allocator, singleton.inner_args())?; + + let DidSolution::InnerSpend(p2_solution) = + DidSolution::::from_clvm(allocator, singleton.inner_solution())?; + + if args.singleton_struct != singleton.args().singleton_struct { + return Err(ParseError::DidSingletonStructMismatch); + } + + let Reduction(_cost, output) = run_program( + allocator, + &ChiaDialect::new(0), + args.inner_puzzle, + p2_solution, + max_cost, + )?; + + let conditions = Vec::::from_clvm(allocator, output)?; + let mut p2_puzzle_hash = None; + + for condition in conditions { + let Ok(create_coin) = CreateCoinWithMemos::from_clvm(allocator, condition) else { + continue; + }; + + if create_coin.amount % 2 == 0 { + continue; + } + + p2_puzzle_hash = create_coin + .memos + .first() + .and_then(|memo| Some(Bytes32::new(memo.as_ref().try_into().ok()?))); + break; + } + + let Some(p2_puzzle_hash) = p2_puzzle_hash else { + return Err(ParseError::MissingCreateCoin); + }; + + let did_inner_puzzle_hash = did_inner_puzzle_hash( + p2_puzzle_hash, + args.recovery_did_list_hash, + args.num_verifications_required, + args.singleton_struct.launcher_id, + tree_hash(allocator, args.metadata).into(), + ); + + let singleton_puzzle_hash = + singleton_puzzle_hash(args.singleton_struct.launcher_id, did_inner_puzzle_hash); + + if singleton_puzzle_hash != ctx.coin().puzzle_hash { + return Err(ParseError::UnknownDidOutput); + } + + Ok(Some(DidInfo { + launcher_id: args.singleton_struct.launcher_id, + coin: ctx.coin(), + p2_puzzle_hash, + did_inner_puzzle_hash, + recovery_did_list_hash: args.recovery_did_list_hash, + num_verifications_required: args.num_verifications_required, + metadata: args.metadata, + proof: Proof::Lineage(LineageProof { + parent_parent_coin_id: ctx.parent_coin().parent_coin_info, + parent_inner_puzzle_hash: tree_hash(allocator, singleton.args().inner_puzzle).into(), + parent_amount: ctx.parent_coin().amount, + }), + })) +} + +#[cfg(test)] +mod tests { + use chia_bls::PublicKey; + use chia_protocol::{Bytes32, Coin}; + use chia_puzzles::standard::{StandardArgs, STANDARD_PUZZLE_HASH}; + use clvm_traits::ToNodePtr; + use clvm_utils::{CurriedProgram, ToTreeHash}; + use clvmr::Allocator; + + use crate::{ + parse_did, parse_puzzle, parse_singleton, Chainable, CreateDid, Launcher, SpendContext, + StandardSpend, + }; + + #[test] + fn test_parse_did() -> anyhow::Result<()> { + let mut allocator = Allocator::new(); + let mut ctx = SpendContext::new(&mut allocator); + + let pk = PublicKey::default(); + let puzzle_hash = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { synthetic_key: pk }, + } + .tree_hash() + .into(); + let parent = Coin::new(Bytes32::default(), puzzle_hash, 1); + + let (create_did, did_info) = Launcher::new(parent.coin_id(), 1) + .create(&mut ctx)? + .create_standard_did(&mut ctx, pk)?; + + StandardSpend::new() + .chain(create_did) + .finish(&mut ctx, parent, pk)?; + + let coin_spends = ctx.take_spends(); + + let coin_spend = coin_spends + .into_iter() + .find(|cs| cs.coin.coin_id() == did_info.coin.parent_coin_info) + .unwrap(); + + let puzzle = coin_spend.puzzle_reveal.to_node_ptr(&mut allocator)?; + let solution = coin_spend.solution.to_node_ptr(&mut allocator)?; + + let parse_ctx = parse_puzzle( + &mut allocator, + puzzle, + solution, + coin_spend.coin, + did_info.coin, + )?; + + let parse = parse_singleton(&mut allocator, &parse_ctx)?.unwrap(); + let parse = parse_did(&mut allocator, &parse_ctx, &parse, u64::MAX)?; + assert_eq!( + parse.map(|did_info| did_info.with_metadata(())), + Some(did_info) + ); + + Ok(()) + } +} diff --git a/src/parser/puzzles/singleton.rs b/src/parser/puzzles/singleton.rs new file mode 100644 index 00000000..2af8dd1a --- /dev/null +++ b/src/parser/puzzles/singleton.rs @@ -0,0 +1,77 @@ +use chia_protocol::Bytes32; +use chia_puzzles::singleton::{ + SingletonArgs, SingletonSolution, SINGLETON_LAUNCHER_PUZZLE_HASH, + SINGLETON_TOP_LAYER_PUZZLE_HASH, +}; +use clvm_traits::FromClvm; +use clvm_utils::{tree_hash, CurriedProgram}; +use clvmr::{Allocator, NodePtr}; + +use crate::{ParseContext, ParseError}; + +pub struct ParseSingleton { + args: SingletonArgs, + solution: SingletonSolution, + inner_mod_hash: Bytes32, + inner_args: NodePtr, + inner_solution: NodePtr, +} + +impl ParseSingleton { + pub fn args(&self) -> &SingletonArgs { + &self.args + } + + pub fn solution(&self) -> &SingletonSolution { + &self.solution + } + + pub fn inner_mod_hash(&self) -> Bytes32 { + self.inner_mod_hash + } + + pub fn inner_args(&self) -> NodePtr { + self.inner_args + } + + pub fn inner_solution(&self) -> NodePtr { + self.inner_solution + } +} + +pub fn parse_singleton( + allocator: &mut Allocator, + ctx: &ParseContext, +) -> Result, ParseError> { + if ctx.mod_hash().to_bytes() != SINGLETON_TOP_LAYER_PUZZLE_HASH.to_bytes() { + return Ok(None); + } + + let singleton_args = SingletonArgs::::from_clvm(allocator, ctx.args())?; + let singleton_solution = SingletonSolution::::from_clvm(allocator, ctx.solution())?; + + let CurriedProgram { program, args } = + CurriedProgram::::from_clvm(allocator, singleton_args.inner_puzzle)?; + + let singleton_mod_hash = singleton_args.singleton_struct.mod_hash.as_ref(); + let launcher_puzzle_hash = singleton_args + .singleton_struct + .launcher_puzzle_hash + .as_ref(); + + if singleton_mod_hash != SINGLETON_TOP_LAYER_PUZZLE_HASH.to_bytes() + || launcher_puzzle_hash != SINGLETON_LAUNCHER_PUZZLE_HASH.to_bytes() + { + return Err(ParseError::InvalidSingletonStruct); + } + + let inner_solution = singleton_solution.inner_solution; + + Ok(Some(ParseSingleton { + args: singleton_args, + solution: singleton_solution, + inner_mod_hash: tree_hash(allocator, program).into(), + inner_args: args, + inner_solution, + })) +} diff --git a/src/spends.rs b/src/spends.rs index 8dde5a70..023c9937 100644 --- a/src/spends.rs +++ b/src/spends.rs @@ -1,235 +1,9 @@ -use std::collections::HashMap; - -use chia_protocol::{Bytes32, Program, SpendBundle}; -use chia_wallet::{ - cat::{CAT_PUZZLE, CAT_PUZZLE_HASH, EVERYTHING_WITH_SIGNATURE_TAIL_PUZZLE}, - did::{DID_INNER_PUZZLE, DID_INNER_PUZZLE_HASH}, - nft::{ - NFT_INTERMEDIATE_LAUNCHER_PUZZLE, NFT_INTERMEDIATE_LAUNCHER_PUZZLE_HASH, - NFT_OWNERSHIP_LAYER_PUZZLE, NFT_OWNERSHIP_LAYER_PUZZLE_HASH, NFT_ROYALTY_TRANSFER_PUZZLE, - NFT_ROYALTY_TRANSFER_PUZZLE_HASH, NFT_STATE_LAYER_PUZZLE, NFT_STATE_LAYER_PUZZLE_HASH, - }, - singleton::{ - SINGLETON_LAUNCHER_PUZZLE, SINGLETON_LAUNCHER_PUZZLE_HASH, SINGLETON_TOP_LAYER_PUZZLE, - SINGLETON_TOP_LAYER_PUZZLE_HASH, - }, - standard::{STANDARD_PUZZLE, STANDARD_PUZZLE_HASH}, -}; -use clvm_traits::{FromClvmError, FromNodePtr, ToClvmError, ToNodePtr}; -use clvm_utils::tree_hash; -use clvmr::{ - reduction::EvalErr, run_program, serde::node_from_bytes, Allocator, ChiaDialect, NodePtr, -}; -use hex_literal::hex; -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -mod cat; -mod did; -mod nft; -mod standard; - -pub use cat::*; -pub use did::*; -pub use nft::*; -pub use standard::*; - -/// Errors that can occur when spending a coin. -#[derive(Debug, Error)] -pub enum SpendError { - /// An error occurred while converting to clvm. - #[error("to clvm error: {0}")] - ToClvm(#[from] ToClvmError), - - /// An error occurred while converting from clvm. - #[error("from clvm error: {0}")] - FromClvm(#[from] FromClvmError), - - /// An error occurred while evaluating a program. - #[error("eval error: {0}")] - Eval(#[from] EvalErr), -} - -/// A wrapper around `Allocator` that caches puzzles and simplifies coin spending. -pub struct SpendContext<'a> { - allocator: &'a mut Allocator, - puzzles: HashMap<[u8; 32], NodePtr>, -} - -impl<'a> SpendContext<'a> { - /// Create a new `SpendContext` from an `Allocator` reference. - pub fn new(allocator: &'a mut Allocator) -> Self { - Self { - allocator, - puzzles: HashMap::new(), - } - } - - /// Allocate a new node and return its pointer. - pub fn alloc(&mut self, value: T) -> Result - where - T: ToNodePtr, - { - Ok(value.to_node_ptr(self.allocator)?) - } - - /// Extract a value from a node pointer. - pub fn extract(&self, ptr: NodePtr) -> Result - where - T: FromNodePtr, - { - Ok(T::from_node_ptr(self.allocator, ptr)?) - } - - /// Compute the tree hash of a node pointer. - pub fn tree_hash(&self, ptr: NodePtr) -> Bytes32 { - Bytes32::new(tree_hash(self.allocator, ptr)) - } - - /// Run a puzzle with a solution and return the result. - pub fn run(&mut self, puzzle: NodePtr, solution: NodePtr) -> Result { - let result = run_program( - self.allocator, - &ChiaDialect::new(0), - puzzle, - solution, - u64::MAX, - )?; - Ok(result.1) - } - - /// Serialize a value and return a `Program`. - pub fn serialize(&mut self, value: T) -> Result - where - T: ToNodePtr, - { - let ptr = value.to_node_ptr(self.allocator)?; - Ok(Program::from_node_ptr(self.allocator, ptr)?) - } - - /// Allocate the standard puzzle and return its pointer. - pub fn standard_puzzle(&mut self) -> NodePtr { - self.puzzle(&STANDARD_PUZZLE_HASH, &STANDARD_PUZZLE) - } - - /// Allocate the CAT puzzle and return its pointer. - pub fn cat_puzzle(&mut self) -> NodePtr { - self.puzzle(&CAT_PUZZLE_HASH, &CAT_PUZZLE) - } - - /// Allocate the DID inner puzzle and return its pointer. - pub fn did_inner_puzzle(&mut self) -> NodePtr { - self.puzzle(&DID_INNER_PUZZLE_HASH, &DID_INNER_PUZZLE) - } - - /// Allocate the NFT intermediate launcher puzzle and return its pointer. - pub fn nft_intermediate_launcher(&mut self) -> NodePtr { - self.puzzle( - &NFT_INTERMEDIATE_LAUNCHER_PUZZLE_HASH, - &NFT_INTERMEDIATE_LAUNCHER_PUZZLE, - ) - } - - /// Allocate the NFT royalty transfer puzzle and return its pointer. - pub fn nft_royalty_transfer(&mut self) -> NodePtr { - self.puzzle( - &NFT_ROYALTY_TRANSFER_PUZZLE_HASH, - &NFT_ROYALTY_TRANSFER_PUZZLE, - ) - } - - /// Allocate the NFT ownership layer puzzle and return its pointer. - pub fn nft_ownership_layer(&mut self) -> NodePtr { - self.puzzle( - &NFT_OWNERSHIP_LAYER_PUZZLE_HASH, - &NFT_OWNERSHIP_LAYER_PUZZLE, - ) - } - - /// Allocate the NFT state layer puzzle and return its pointer. - pub fn nft_state_layer(&mut self) -> NodePtr { - self.puzzle(&NFT_STATE_LAYER_PUZZLE_HASH, &NFT_STATE_LAYER_PUZZLE) - } - - /// Allocate the singleton top layer puzzle and return its pointer. - pub fn singleton_top_layer(&mut self) -> NodePtr { - self.puzzle( - &SINGLETON_TOP_LAYER_PUZZLE_HASH, - &SINGLETON_TOP_LAYER_PUZZLE, - ) - } - - /// Allocate the singleton launcher puzzle and return its pointer. - pub fn singleton_launcher(&mut self) -> NodePtr { - self.puzzle(&SINGLETON_LAUNCHER_PUZZLE_HASH, &SINGLETON_LAUNCHER_PUZZLE) - } - - /// Allocate the EverythingWithSignature TAIL puzzle and return its pointer. - pub fn everything_with_signature_tail_puzzle(&mut self) -> NodePtr { - // todo: add constant to chia_rs - self.puzzle( - &hex!("1720d13250a7c16988eaf530331cefa9dd57a76b2c82236bec8bbbff91499b89"), - &EVERYTHING_WITH_SIGNATURE_TAIL_PUZZLE, - ) - } - - /// Preload a puzzle into the cache. - pub fn preload(&mut self, puzzle_hash: [u8; 32], ptr: NodePtr) { - self.puzzles.insert(puzzle_hash, ptr); - } - - /// Get a puzzle from the cache or allocate a new one. - pub fn puzzle(&mut self, puzzle_hash: &[u8; 32], puzzle_bytes: &[u8]) -> NodePtr { - if let Some(puzzle) = self.puzzles.get(puzzle_bytes) { - *puzzle - } else { - let puzzle = node_from_bytes(self.allocator, puzzle_bytes).unwrap(); - self.puzzles.insert(*puzzle_hash, puzzle); - puzzle - } - } -} - -#[derive(Serialize, Deserialize)] -struct CoinJson { - parent_coin_info: String, - puzzle_hash: String, - amount: u64, -} - -#[derive(Serialize, Deserialize)] -struct CoinSpendJson { - coin: CoinJson, - puzzle_reveal: String, - solution: String, -} - -#[derive(Serialize, Deserialize)] -struct SpendBundleJson { - coin_spends: Vec, - aggregated_signature: String, -} - -/// Dump a `SpendBundle` to a JSON string. -pub fn dump_spend_bundle(bundle: &SpendBundle) -> String { - let mut coin_spends = Vec::new(); - - for coin_spend in &bundle.coin_spends { - coin_spends.push(CoinSpendJson { - coin: CoinJson { - parent_coin_info: format!("0x{}", hex::encode(coin_spend.coin.parent_coin_info)), - puzzle_hash: format!("0x{}", hex::encode(coin_spend.coin.puzzle_hash)), - amount: coin_spend.coin.amount, - }, - puzzle_reveal: hex::encode(&coin_spend.puzzle_reveal), - solution: hex::encode(&coin_spend.solution), - }); - } - - let json = SpendBundleJson { - coin_spends, - aggregated_signature: hex::encode(bundle.aggregated_signature.to_bytes()), - }; - - serde_json::to_string(&json).unwrap() -} +mod puzzles; +mod spend_builder; +mod spend_context; +mod spend_error; + +pub use puzzles::*; +pub use spend_builder::*; +pub use spend_context::*; +pub use spend_error::*; diff --git a/src/spends/cat.rs b/src/spends/cat.rs deleted file mode 100644 index 65b7ba19..00000000 --- a/src/spends/cat.rs +++ /dev/null @@ -1,447 +0,0 @@ -use chia_bls::PublicKey; -use chia_protocol::{Bytes32, Coin, CoinSpend}; -use chia_wallet::{ - cat::{CatArgs, CatSolution, CoinProof, EverythingWithSignatureTailArgs, CAT_PUZZLE_HASH}, - standard::{StandardArgs, StandardSolution}, - LineageProof, -}; -use clvm_traits::{clvm_quote, destructure_tuple, match_tuple, MatchByte, ToClvm}; -use clvm_utils::CurriedProgram; -use clvmr::NodePtr; - -use crate::{RunTail, SpendContext, SpendError}; - -/// The information required to spend a CAT coin. -/// This assumes that the inner puzzle is a standard transaction. -pub struct CatSpend { - /// The CAT coin that is being spent. - pub coin: Coin, - /// The public key used for the inner puzzle. - pub synthetic_key: PublicKey, - /// The desired output conditions for the coin spend. - pub conditions: NodePtr, - /// The extra delta produced as part of this spend. - pub extra_delta: i64, - /// The inner puzzle hash. - pub p2_puzzle_hash: Bytes32, - /// The lineage proof of the CAT. - pub lineage_proof: LineageProof, -} - -/// Creates a set of CAT coin spends for a given asset id. -pub fn spend_cat_coins( - ctx: &mut SpendContext, - asset_id: Bytes32, - cat_spends: &[CatSpend], -) -> Result, SpendError> { - let cat_puzzle_ptr = ctx.cat_puzzle(); - let standard_puzzle_ptr = ctx.standard_puzzle(); - - let mut coin_spends = Vec::new(); - let mut total_delta = 0; - let len = cat_spends.len(); - - for (index, cat_spend) in cat_spends.iter().enumerate() { - // Calculate the delta and add it to the subtotal. - let conditions: Vec = ctx.extract(cat_spend.conditions)?; - let create_coins = conditions.into_iter().filter_map(|ptr| { - ctx.extract::, NodePtr, u64, NodePtr)>(ptr) - .ok() - }); - let delta = create_coins.fold( - cat_spend.coin.amount as i64 - cat_spend.extra_delta, - |delta, destructure_tuple!(_, _, amount, _)| delta - amount as i64, - ); - - let prev_subtotal = total_delta; - total_delta += delta; - - // Find information of neighboring coins on the ring. - let prev_cat = &cat_spends[if index == 0 { len - 1 } else { index - 1 }]; - let next_cat = &cat_spends[if index == len - 1 { 0 } else { index + 1 }]; - - let puzzle_reveal = ctx.serialize(CurriedProgram { - program: cat_puzzle_ptr, - args: CatArgs { - mod_hash: CAT_PUZZLE_HASH.into(), - tail_program_hash: asset_id, - inner_puzzle: CurriedProgram { - program: standard_puzzle_ptr, - args: StandardArgs { - synthetic_key: cat_spend.synthetic_key.clone(), - }, - }, - }, - })?; - - let solution = ctx.serialize(CatSolution { - inner_puzzle_solution: StandardSolution { - original_public_key: None, - delegated_puzzle: clvm_quote!(&cat_spend.conditions), - solution: (), - }, - lineage_proof: Some(cat_spend.lineage_proof.clone()), - prev_coin_id: prev_cat.coin.coin_id(), - this_coin_info: cat_spend.coin.clone(), - next_coin_proof: CoinProof { - parent_coin_info: next_cat.coin.parent_coin_info, - inner_puzzle_hash: next_cat.p2_puzzle_hash, - amount: next_cat.coin.amount, - }, - prev_subtotal, - extra_delta: cat_spend.extra_delta, - })?; - - coin_spends.push(CoinSpend::new( - cat_spend.coin.clone(), - puzzle_reveal, - solution, - )); - } - - Ok(coin_spends) -} - -/// The information required to create and spend an eve CAT coin. -pub struct EveSpend { - /// The full puzzle hash of the eve CAT coin. - pub puzzle_hash: Bytes32, - /// The coin spend for the eve CAT. - pub coin_spend: CoinSpend, -} - -/// Constructs a coin spend to issue more of an `EverythingWithSignature` CAT. -pub fn issue_cat_everything_with_signature( - ctx: &mut SpendContext, - public_key: PublicKey, - parent_coin_id: Bytes32, - amount: u64, - conditions: T, -) -> Result -where - T: ToClvm, -{ - let tail_puzzle_ptr = ctx.everything_with_signature_tail_puzzle(); - - let tail = ctx.alloc(CurriedProgram { - program: tail_puzzle_ptr, - args: EverythingWithSignatureTailArgs { public_key }, - })?; - let asset_id = ctx.tree_hash(tail); - - let run_tail = RunTail { - program: tail, - solution: NodePtr::NIL, - }; - - let conditions = (run_tail, conditions); - - create_and_spend_eve_cat(ctx, parent_coin_id, asset_id, amount, conditions) -} - -/// Creates an eve CAT coin and spends it. -pub fn create_and_spend_eve_cat( - ctx: &mut SpendContext, - parent_coin_id: Bytes32, - asset_id: Bytes32, - amount: u64, - conditions: T, -) -> Result -where - T: ToClvm, -{ - let cat_puzzle_ptr = ctx.cat_puzzle(); - - let inner_puzzle = ctx.alloc(clvm_quote!(conditions))?; - let inner_puzzle_hash = ctx.tree_hash(inner_puzzle); - - let puzzle = ctx.alloc(CurriedProgram { - program: cat_puzzle_ptr, - args: CatArgs { - mod_hash: CAT_PUZZLE_HASH.into(), - tail_program_hash: asset_id, - inner_puzzle, - }, - })?; - - let puzzle_hash = ctx.tree_hash(puzzle); - let coin = Coin::new(parent_coin_id, puzzle_hash, amount); - - let solution = ctx.serialize(CatSolution { - inner_puzzle_solution: (), - lineage_proof: None, - prev_coin_id: coin.coin_id(), - this_coin_info: coin.clone(), - next_coin_proof: CoinProof { - parent_coin_info: parent_coin_id, - inner_puzzle_hash, - amount, - }, - prev_subtotal: 0, - extra_delta: 0, - })?; - - let puzzle_reveal = ctx.serialize(puzzle)?; - let coin_spend = CoinSpend::new(coin, puzzle_reveal, solution); - - Ok(EveSpend { - puzzle_hash, - coin_spend, - }) -} - -#[cfg(test)] -mod tests { - use chia_bls::derive_keys::master_to_wallet_unhardened; - use chia_consensus::gen::{ - conditions::EmptyVisitor, run_block_generator::run_block_generator, - solution_generator::solution_generator, - }; - use chia_protocol::{Bytes32, Program}; - use chia_wallet::{ - cat::cat_puzzle_hash, - standard::{standard_puzzle_hash, DEFAULT_HIDDEN_PUZZLE_HASH}, - DeriveSynthetic, - }; - use clvmr::{serde::node_to_bytes, Allocator}; - use hex_literal::hex; - - use crate::{testing::SECRET_KEY, CreateCoinWithoutMemos}; - - use super::*; - - #[test] - fn test_cat_spend() { - let synthetic_key = master_to_wallet_unhardened(&SECRET_KEY.public_key(), 0) - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH); - - let mut allocator = Allocator::new(); - let mut ctx = SpendContext::new(&mut allocator); - - let asset_id = Bytes32::new([42; 32]); - - let p2_puzzle_hash = Bytes32::new(standard_puzzle_hash(&synthetic_key)); - let cat_puzzle_hash = cat_puzzle_hash(asset_id.to_bytes(), p2_puzzle_hash.to_bytes()); - - let parent_coin = Coin::new(Bytes32::new([0; 32]), Bytes32::new(cat_puzzle_hash), 69); - let coin = Coin::new( - Bytes32::from(parent_coin.coin_id()), - Bytes32::new(cat_puzzle_hash), - 42, - ); - - let conditions = ctx - .alloc([CreateCoinWithoutMemos { - puzzle_hash: coin.puzzle_hash, - amount: coin.amount, - }]) - .unwrap(); - - let coin_spend = spend_cat_coins( - &mut ctx, - asset_id, - &[CatSpend { - coin, - synthetic_key, - conditions, - extra_delta: 0, - lineage_proof: LineageProof { - parent_coin_info: parent_coin.parent_coin_info, - inner_puzzle_hash: p2_puzzle_hash, - amount: parent_coin.amount, - }, - p2_puzzle_hash, - }], - ) - .unwrap() - .remove(0); - - let output_ptr = coin_spend - .puzzle_reveal - .run(&mut allocator, 0, u64::MAX, &coin_spend.solution) - .unwrap() - .1; - let actual = node_to_bytes(&allocator, output_ptr).unwrap(); - - let expected = hex!( - " - ffff46ffa06438c882c2db9f5c2a8b4cbda9258c40a6583b2d7c6becc1678607 - 4d558c834980ffff3cffa1cb9c4d253a0e1a091d620a55616e104f3329f58ee8 - 6e708d0527b1cc58a73b649e80ffff3dffa0c3bb7f0a7e1bd2cae332bbd0d1a7 - e275c1e6c643b2659e22c24f513886d3874e80ffff32ffb08584adae5630842a - 1766bc444d2b872dd3080f4e5daaecf6f762a4be7dc148f37868149d4217f3dc - c9183fe61e48d8bfffa0e5924c23faf33c9a1bf18c70d40cb09e4b194f521b9f - 6fceb2685c0612ac34a980ffff33ffa0f9f2d59294f2aae8f9833db876d1bf43 - 95d46af18c17312041c6f4a4d73fa041ff2a8080 - " - ); - assert_eq!(hex::encode(actual), hex::encode(expected)); - } - - #[test] - fn test_cat_spend_multi() { - let synthetic_key = master_to_wallet_unhardened(&SECRET_KEY.public_key(), 0) - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH); - - let mut allocator = Allocator::new(); - let mut ctx = SpendContext::new(&mut allocator); - - let asset_id = Bytes32::new([42; 32]); - - let p2_puzzle_hash = Bytes32::new(standard_puzzle_hash(&synthetic_key)); - let cat_puzzle_hash = cat_puzzle_hash(asset_id.to_bytes(), p2_puzzle_hash.to_bytes()); - - let parent_coin_1 = Coin::new(Bytes32::new([0; 32]), Bytes32::new(cat_puzzle_hash), 69); - let coin_1 = Coin::new( - Bytes32::from(parent_coin_1.coin_id()), - Bytes32::new(cat_puzzle_hash), - 42, - ); - - let parent_coin_2 = Coin::new(Bytes32::new([0; 32]), Bytes32::new(cat_puzzle_hash), 69); - let coin_2 = Coin::new( - Bytes32::from(parent_coin_2.coin_id()), - Bytes32::new(cat_puzzle_hash), - 34, - ); - - let parent_coin_3 = Coin::new(Bytes32::new([0; 32]), Bytes32::new(cat_puzzle_hash), 69); - let coin_3 = Coin::new( - Bytes32::from(parent_coin_3.coin_id()), - Bytes32::new(cat_puzzle_hash), - 69, - ); - - let conditions = ctx - .alloc([CreateCoinWithoutMemos { - puzzle_hash: coin_1.puzzle_hash, - amount: coin_1.amount + coin_2.amount + coin_3.amount, - }]) - .unwrap(); - - let coin_spends = spend_cat_coins( - &mut ctx, - asset_id, - &[ - CatSpend { - coin: coin_1, - synthetic_key: synthetic_key.clone(), - conditions, - extra_delta: 0, - lineage_proof: LineageProof { - parent_coin_info: parent_coin_1.parent_coin_info, - inner_puzzle_hash: p2_puzzle_hash, - amount: parent_coin_1.amount, - }, - p2_puzzle_hash, - }, - CatSpend { - coin: coin_2, - synthetic_key: synthetic_key.clone(), - conditions: NodePtr::NIL, - extra_delta: 0, - lineage_proof: LineageProof { - parent_coin_info: parent_coin_2.parent_coin_info, - inner_puzzle_hash: p2_puzzle_hash, - amount: parent_coin_2.amount, - }, - p2_puzzle_hash, - }, - CatSpend { - coin: coin_3, - synthetic_key, - conditions: NodePtr::NIL, - extra_delta: 0, - lineage_proof: LineageProof { - parent_coin_info: parent_coin_3.parent_coin_info, - inner_puzzle_hash: p2_puzzle_hash, - amount: parent_coin_3.amount, - }, - p2_puzzle_hash, - }, - ], - ) - .unwrap(); - - let spend_vec = coin_spends - .clone() - .into_iter() - .map(|coin_spend| { - ( - coin_spend.coin, - coin_spend.puzzle_reveal, - coin_spend.solution, - ) - }) - .collect::>(); - let gen = solution_generator(spend_vec).unwrap(); - let block = - run_block_generator::(&mut allocator, &gen, &[], u64::MAX, 0) - .unwrap(); - - assert_eq!(block.cost, 101289468); - - assert_eq!(coin_spends.len(), 3); - - let output_ptr_1 = coin_spends[0] - .puzzle_reveal - .run(&mut allocator, 0, u64::MAX, &coin_spends[0].solution) - .unwrap() - .1; - let actual = node_to_bytes(&allocator, output_ptr_1).unwrap(); - - let expected = hex!( - " - ffff46ffa06438c882c2db9f5c2a8b4cbda9258c40a6583b2d7c6becc1678607 - 4d558c834980ffff3cffa1cb1cb6597fe61e67a6cbbcd4e8f0bda5e9fc56cd84 - c9e9502772b410dc8a03207680ffff3dffa0742ddb368882193072ea013bde24 - 4a5c9d40ab4454c09666e84777a79307e17a80ffff32ffb08584adae5630842a - 1766bc444d2b872dd3080f4e5daaecf6f762a4be7dc148f37868149d4217f3dc - c9183fe61e48d8bfffa004c476adfcffeacfef7c979bdd03b4641f1870d3f81b - 20636eefbcf879bb64ec80ffff33ffa0f9f2d59294f2aae8f9833db876d1bf43 - 95d46af18c17312041c6f4a4d73fa041ff8200918080 - " - ); - assert_eq!(hex::encode(actual), hex::encode(expected)); - - let output_ptr_2 = coin_spends[1] - .puzzle_reveal - .run(&mut allocator, 0, u64::MAX, &coin_spends[1].solution) - .unwrap() - .1; - let actual = node_to_bytes(&allocator, output_ptr_2).unwrap(); - - let expected = hex!( - " - ffff46ffa0ae60b8db0664959078a1c6e51ca6a8fc55207c63a8ac74d026f1d9 - 15c406bac480ffff3cffa1cb9a41843ab318a8336f61a6bf9e8b0b1d555b9f07 - cd19582e0bc52a961c65dc9e80ffff3dffa0294cda8d35164e01c4e3b7c07c36 - a5bb2f38a23e93ef49c882ee74349a0df8bd80ffff32ffb08584adae5630842a - 1766bc444d2b872dd3080f4e5daaecf6f762a4be7dc148f37868149d4217f3dc - c9183fe61e48d8bfffa0ba4484b961b7a2369d948d06c55b64bdbfaffb326bc1 - 3b490ab1215dd33d8d468080 - " - ); - assert_eq!(hex::encode(actual), hex::encode(expected)); - - let output_ptr_3 = coin_spends[2] - .puzzle_reveal - .run(&mut allocator, 0, u64::MAX, &coin_spends[2].solution) - .unwrap() - .1; - let actual = node_to_bytes(&allocator, output_ptr_3).unwrap(); - - let expected = hex!( - " - ffff46ffa0f8eacbef2bad0c7b27b638a90a37244e75013e977f250230856d05 - a2784e1d0980ffff3cffa1cb17c47c5fa8d795efa0d9227d2066cde36dd4e845 - 7e8f4e507d2015a1c7f3d94b80ffff3dffa0629abc502829339c7880ee003c4e - 68a8181d71206e50e7b36c29301ef60128f580ffff32ffb08584adae5630842a - 1766bc444d2b872dd3080f4e5daaecf6f762a4be7dc148f37868149d4217f3dc - c9183fe61e48d8bfffa0ba4484b961b7a2369d948d06c55b64bdbfaffb326bc1 - 3b490ab1215dd33d8d468080 - " - ); - assert_eq!(hex::encode(actual), hex::encode(expected)); - } -} diff --git a/src/spends/did.rs b/src/spends/did.rs deleted file mode 100644 index ba079563..00000000 --- a/src/spends/did.rs +++ /dev/null @@ -1,29 +0,0 @@ -use chia_protocol::{Coin, CoinSpend, Program}; -use chia_wallet::{did::DidSolution, singleton::SingletonSolution, Proof}; -use clvm_traits::ToClvm; -use clvmr::NodePtr; - -use crate::{standard_solution, SpendContext, SpendError}; - -/// Spend a standard DID coin (a DID singleton with the standard transaction inner puzzle). -pub fn spend_did( - ctx: &mut SpendContext, - coin: Coin, - puzzle_reveal: Program, - proof: Proof, - conditions: T, -) -> Result -where - T: ToClvm, -{ - let p2_solution = standard_solution(conditions); - let did_solution = DidSolution::InnerSpend(p2_solution); - - let solution = ctx.serialize(SingletonSolution { - proof, - amount: coin.amount, - inner_solution: did_solution, - })?; - - Ok(CoinSpend::new(coin, puzzle_reveal, solution)) -} diff --git a/src/spends/nft.rs b/src/spends/nft.rs deleted file mode 100644 index 506cf473..00000000 --- a/src/spends/nft.rs +++ /dev/null @@ -1,321 +0,0 @@ -use chia_bls::PublicKey; -use chia_protocol::{Bytes, Bytes32, Coin, CoinSpend, Program}; -use chia_wallet::{ - nft::{ - NftIntermediateLauncherArgs, NftOwnershipLayerArgs, NftOwnershipLayerSolution, - NftRoyaltyTransferPuzzleArgs, NftStateLayerArgs, NftStateLayerSolution, - NFT_METADATA_UPDATER_PUZZLE_HASH, NFT_OWNERSHIP_LAYER_PUZZLE_HASH, - NFT_STATE_LAYER_PUZZLE_HASH, - }, - singleton::{ - LauncherSolution, SingletonArgs, SingletonSolution, SingletonStruct, - SINGLETON_LAUNCHER_PUZZLE_HASH, SINGLETON_TOP_LAYER_PUZZLE_HASH, - }, - standard::{StandardArgs, StandardSolution}, - EveProof, Proof, -}; -use clvm_traits::{clvm_list, clvm_quote, ToClvm}; -use clvm_utils::CurriedProgram; -use clvmr::{ - sha2::{Digest, Sha256}, - NodePtr, -}; - -use crate::{ - trim_leading_zeros, AssertCoinAnnouncement, AssertPuzzleAnnouncement, CreateCoinWithMemos, - CreateCoinWithoutMemos, CreatePuzzleAnnouncement, NewNftOwner, SpendContext, SpendError, -}; - -/// Spend an NFT. -pub fn spend_nft( - ctx: &mut SpendContext, - coin: Coin, - puzzle_reveal: Program, - proof: Proof, - conditions: T, -) -> Result -where - T: ToClvm, -{ - // Construct the p2 solution. - let p2_solution = StandardSolution { - original_public_key: None, - delegated_puzzle: clvm_quote!(conditions), - solution: (), - }; - - // Construct the ownership layer solution. - let ownership_layer_solution = NftOwnershipLayerSolution { - inner_solution: p2_solution, - }; - - // Construct the state layer solution. - let state_layer_solution = NftStateLayerSolution { - inner_solution: ownership_layer_solution, - }; - - // Construct the singleton solution. - let solution = ctx.serialize(SingletonSolution { - proof, - amount: coin.amount, - inner_solution: state_layer_solution, - })?; - - // Construct the coin spend. - let coin_spend = CoinSpend::new(coin, puzzle_reveal, solution); - - Ok(coin_spend) -} - -/// The information required to mint an NFT. -pub struct MintInput { - /// The owner puzzle hash of the newly minted NFT. - pub owner_puzzle_hash: Bytes32, - /// The puzzle hash to send royalties to when trading the NFT. - pub royalty_puzzle_hash: Bytes32, - /// The percentage royalty to send to the royalty puzzle hash. - pub royalty_percentage: u16, - /// The NFT metadata. - pub metadata: NodePtr, - /// The parent coin to spend. - pub parent_coin_id: Bytes32, - /// The amount of the launcher coin and subsequent NFT coin. - pub amount: u64, -} - -/// The information required to create and spend an NFT bulk mint. -pub struct BulkMint { - /// The coin spends for the NFT bulk mint. - pub coin_spends: Vec, - /// The new NFT outputs. - pub outputs: Vec, -} - -/// The output of a single NFT mint. -pub struct MintOutput { - /// The conditions that must be output from the parent to make this mint valid. - pub parent_conditions: Vec, - /// The launcher id of the newly minted NFT. - pub launcher_id: Bytes32, -} - -/// Bulk mints a set of NFTs. -pub fn mint_nfts( - ctx: &mut SpendContext, - inputs: Vec, - synthetic_key: PublicKey, - did_id: Bytes32, - did_inner_puzzle_hash: Bytes32, -) -> Result { - let mut coin_spends = Vec::new(); - let mut outputs = Vec::new(); - - let standard_puzzle = ctx.standard_puzzle(); - let royalty_transfer_puzzle = ctx.nft_royalty_transfer(); - let ownership_puzzle = ctx.nft_ownership_layer(); - let state_puzzle = ctx.nft_state_layer(); - let singleton_puzzle = ctx.singleton_top_layer(); - let launcher_puzzle = ctx.singleton_launcher(); - - let p2 = ctx.alloc(CurriedProgram { - program: standard_puzzle, - args: StandardArgs { synthetic_key }, - })?; - - let mint_total = inputs.len(); - - for (mint_index, input) in inputs.into_iter().enumerate() { - let mut parent_conditions = Vec::new(); - - // Create the intermediate launcher. - let intermediate_spend = - spend_new_intermediate_launcher(ctx, input.parent_coin_id, mint_index, mint_total)?; - let intermediate_id = intermediate_spend.coin.coin_id(); - - parent_conditions.push(ctx.alloc(CreateCoinWithoutMemos { - puzzle_hash: intermediate_spend.coin.puzzle_hash, - amount: intermediate_spend.coin.amount, - })?); - - let mut index_message = Sha256::new(); - index_message.update(usize_to_bytes(mint_index)); - index_message.update(usize_to_bytes(mint_total)); - - let mut announcement_id = Sha256::new(); - announcement_id.update(intermediate_id); - announcement_id.update(index_message.finalize()); - - parent_conditions.push(ctx.alloc(AssertCoinAnnouncement { - announcement_id: Bytes::new(announcement_id.finalize().to_vec()), - })?); - - coin_spends.push(intermediate_spend); - - // Construct the eve NFT. - let launcher_coin = Coin::new( - intermediate_id, - SINGLETON_LAUNCHER_PUZZLE_HASH.into(), - input.amount, - ); - let launcher_id = launcher_coin.coin_id(); - - parent_conditions.push(ctx.alloc(CreatePuzzleAnnouncement { - message: launcher_id.to_vec().into(), - })?); - - let singleton_struct = SingletonStruct { - mod_hash: SINGLETON_TOP_LAYER_PUZZLE_HASH.into(), - launcher_id, - launcher_puzzle_hash: SINGLETON_LAUNCHER_PUZZLE_HASH.into(), - }; - - let royalty_transfer = CurriedProgram { - program: royalty_transfer_puzzle, - args: NftRoyaltyTransferPuzzleArgs { - singleton_struct: singleton_struct.clone(), - royalty_puzzle_hash: input.royalty_puzzle_hash, - trade_price_percentage: input.royalty_percentage, - }, - }; - - let ownership_layer = CurriedProgram { - program: ownership_puzzle, - args: NftOwnershipLayerArgs { - mod_hash: NFT_OWNERSHIP_LAYER_PUZZLE_HASH.into(), - current_owner: None, - transfer_program: royalty_transfer, - inner_puzzle: p2, - }, - }; - - let state_layer = CurriedProgram { - program: state_puzzle, - args: NftStateLayerArgs { - mod_hash: NFT_STATE_LAYER_PUZZLE_HASH.into(), - metadata: input.metadata, - metadata_updater_puzzle_hash: NFT_METADATA_UPDATER_PUZZLE_HASH.into(), - inner_puzzle: ownership_layer, - }, - }; - - let singleton = ctx.alloc(CurriedProgram { - program: singleton_puzzle, - args: SingletonArgs { - singleton_struct, - inner_puzzle: state_layer, - }, - })?; - - let eve_puzzle_hash = ctx.tree_hash(singleton); - - let eve_message = ctx.alloc(clvm_list!(eve_puzzle_hash, input.amount, ()))?; - let eve_message_hash = ctx.tree_hash(eve_message); - - let mut announcement_id = Sha256::new(); - announcement_id.update(launcher_id); - announcement_id.update(eve_message_hash); - - parent_conditions.push(ctx.alloc(AssertCoinAnnouncement { - announcement_id: Bytes::new(announcement_id.finalize().to_vec()), - })?); - - // Spend the launcher coin. - let launcher_puzzle_reveal = ctx.serialize(launcher_puzzle)?; - let launcher_solution = ctx.serialize(LauncherSolution { - singleton_puzzle_hash: eve_puzzle_hash, - amount: input.amount, - key_value_list: (), - })?; - - coin_spends.push(CoinSpend::new( - launcher_coin, - launcher_puzzle_reveal, - launcher_solution, - )); - - // Spend the eve coin. - let eve_coin = Coin::new(launcher_id, eve_puzzle_hash, input.amount); - - let eve_proof = Proof::Eve(EveProof { - parent_coin_info: intermediate_id, - amount: input.amount, - }); - - let eve_puzzle_reveal = ctx.serialize(singleton)?; - - let eve_coin_spend = spend_nft( - ctx, - eve_coin, - eve_puzzle_reveal, - eve_proof, - clvm_list!( - CreateCoinWithMemos { - puzzle_hash: input.owner_puzzle_hash, - amount: input.amount, - memos: vec![Bytes::new(input.owner_puzzle_hash.to_vec())], - }, - NewNftOwner { - new_owner: Some(did_id), - trade_prices_list: Vec::new(), - new_did_inner_hash: Some(did_inner_puzzle_hash) - } - ), - )?; - let new_nft_owner_args = ctx.alloc(clvm_list!(did_id, (), did_inner_puzzle_hash))?; - - coin_spends.push(eve_coin_spend); - - let mut announcement_id = Sha256::new(); - announcement_id.update(eve_puzzle_hash); - announcement_id.update([0xad, 0x4c]); - announcement_id.update(ctx.tree_hash(new_nft_owner_args)); - - parent_conditions.push(ctx.alloc(AssertPuzzleAnnouncement { - announcement_id: Bytes::new(announcement_id.finalize().to_vec()), - })?); - - // Finalize the output. - outputs.push(MintOutput { - parent_conditions, - launcher_id, - }); - } - - Ok(BulkMint { - coin_spends, - outputs, - }) -} - -fn spend_new_intermediate_launcher( - ctx: &mut SpendContext, - parent_coin_id: Bytes32, - index: usize, - total: usize, -) -> Result { - let intermediate_puzzle = ctx.nft_intermediate_launcher(); - - let puzzle = ctx.alloc(CurriedProgram { - program: intermediate_puzzle, - args: NftIntermediateLauncherArgs { - launcher_puzzle_hash: SINGLETON_LAUNCHER_PUZZLE_HASH.into(), - mint_number: index, - mint_total: total, - }, - })?; - let puzzle_reveal = ctx.serialize(puzzle)?; - let solution = ctx.serialize(())?; - - let puzzle_hash = ctx.tree_hash(puzzle); - - Ok(CoinSpend::new( - Coin::new(parent_coin_id, puzzle_hash, 0), - puzzle_reveal, - solution, - )) -} - -fn usize_to_bytes(amount: usize) -> Vec { - let bytes: Vec = amount.to_be_bytes().into(); - trim_leading_zeros(bytes.as_slice()).to_vec() -} diff --git a/src/spends/puzzles.rs b/src/spends/puzzles.rs new file mode 100644 index 00000000..79f67065 --- /dev/null +++ b/src/spends/puzzles.rs @@ -0,0 +1,13 @@ +mod cat; +mod did; +mod nft; +mod offer; +mod singleton; +mod standard; + +pub use cat::*; +pub use did::*; +pub use nft::*; +pub use offer::*; +pub use singleton::*; +pub use standard::*; diff --git a/src/spends/puzzles/cat.rs b/src/spends/puzzles/cat.rs new file mode 100644 index 00000000..f3c65c5a --- /dev/null +++ b/src/spends/puzzles/cat.rs @@ -0,0 +1,7 @@ +mod cat_info; +mod cat_spend; +mod issue_cat; + +pub use cat_info::*; +pub use cat_spend::*; +pub use issue_cat::*; diff --git a/src/spends/puzzles/cat/cat_info.rs b/src/spends/puzzles/cat/cat_info.rs new file mode 100644 index 00000000..fa23b9d4 --- /dev/null +++ b/src/spends/puzzles/cat/cat_info.rs @@ -0,0 +1,10 @@ +use chia_protocol::{Bytes32, Coin}; +use chia_puzzles::LineageProof; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CatInfo { + pub asset_id: Bytes32, + pub p2_puzzle_hash: Bytes32, + pub coin: Coin, + pub lineage_proof: LineageProof, +} diff --git a/src/spends/puzzles/cat/cat_spend.rs b/src/spends/puzzles/cat/cat_spend.rs new file mode 100644 index 00000000..a3adb0d3 --- /dev/null +++ b/src/spends/puzzles/cat/cat_spend.rs @@ -0,0 +1,357 @@ +use chia_protocol::{Bytes32, Coin, CoinSpend}; +use chia_puzzles::{ + cat::{CatArgs, CatSolution, CoinProof, CAT_PUZZLE_HASH}, + LineageProof, +}; +use clvm_utils::CurriedProgram; +use clvmr::NodePtr; + +use crate::{CreateCoin, InnerSpend, SpendContext, SpendError}; + +#[derive(Default)] +pub struct CatSpend { + asset_id: Bytes32, + cat_spends: Vec, +} + +struct CatSpendItem { + coin: Coin, + inner_spend: InnerSpend, + lineage_proof: LineageProof, + extra_delta: i64, +} + +impl CatSpend { + pub fn new(asset_id: Bytes32) -> Self { + Self { + asset_id, + cat_spends: Vec::new(), + } + } + + pub fn spend( + mut self, + coin: Coin, + inner_spend: InnerSpend, + lineage_proof: LineageProof, + extra_delta: i64, + ) -> Self { + self.cat_spends.push(CatSpendItem { + coin, + inner_spend, + lineage_proof, + extra_delta, + }); + self + } + + pub fn finish(self, ctx: &mut SpendContext) -> Result<(), SpendError> { + let cat_puzzle_ptr = ctx.cat_puzzle(); + let len = self.cat_spends.len(); + + let mut total_delta = 0; + + for (index, item) in self.cat_spends.iter().enumerate() { + let CatSpendItem { + coin, + inner_spend, + lineage_proof, + extra_delta, + } = item; + + // Calculate the delta and add it to the subtotal. + let output = ctx.run(inner_spend.puzzle(), inner_spend.solution())?; + let conditions: Vec = ctx.extract(output)?; + + let create_coins = conditions + .into_iter() + .filter_map(|ptr| ctx.extract::(ptr).ok()); + + let delta = create_coins + .fold(coin.amount as i64 - *extra_delta, |delta, create_coin| { + delta - create_coin.amount() as i64 + }); + + let prev_subtotal = total_delta; + total_delta += delta; + + // Find information of neighboring coins on the ring. + let prev_cat = &self.cat_spends[if index == 0 { len - 1 } else { index - 1 }]; + let next_cat = &self.cat_spends[if index == len - 1 { 0 } else { index + 1 }]; + + let puzzle_reveal = ctx.serialize(CurriedProgram { + program: cat_puzzle_ptr, + args: CatArgs { + mod_hash: CAT_PUZZLE_HASH.into(), + tail_program_hash: self.asset_id, + inner_puzzle: inner_spend.puzzle(), + }, + })?; + + let solution = ctx.serialize(CatSolution { + inner_puzzle_solution: inner_spend.solution(), + lineage_proof: Some(*lineage_proof), + prev_coin_id: prev_cat.coin.coin_id(), + this_coin_info: *coin, + next_coin_proof: CoinProof { + parent_coin_info: next_cat.coin.parent_coin_info, + inner_puzzle_hash: ctx.tree_hash(inner_spend.puzzle()).into(), + amount: next_cat.coin.amount, + }, + prev_subtotal, + extra_delta: *extra_delta, + })?; + + ctx.spend(CoinSpend::new(*coin, puzzle_reveal, solution)); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use chia_bls::derive_keys::master_to_wallet_unhardened; + use chia_consensus::gen::{ + conditions::EmptyVisitor, run_block_generator::run_block_generator, + solution_generator::solution_generator, + }; + use chia_protocol::{Bytes32, Program}; + use chia_puzzles::{ + standard::{StandardArgs, STANDARD_PUZZLE_HASH}, + DeriveSynthetic, + }; + use clvm_utils::ToTreeHash; + use clvmr::{serde::node_to_bytes, Allocator}; + use hex_literal::hex; + + use crate::{testing::SECRET_KEY, Chainable, CreateCoinWithoutMemos, StandardSpend}; + + use super::*; + + #[test] + fn test_cat_spend() -> anyhow::Result<()> { + let synthetic_key = + master_to_wallet_unhardened(&SECRET_KEY.public_key(), 0).derive_synthetic(); + + let mut allocator = Allocator::new(); + let mut ctx = SpendContext::new(&mut allocator); + + let asset_id = Bytes32::new([42; 32]); + + let p2_puzzle_hash = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { synthetic_key }, + } + .tree_hash(); + + let cat_puzzle_hash = CurriedProgram { + program: CAT_PUZZLE_HASH, + args: CatArgs { + mod_hash: CAT_PUZZLE_HASH.into(), + tail_program_hash: asset_id, + inner_puzzle: p2_puzzle_hash, + }, + } + .tree_hash(); + + let parent_coin = Coin::new(Bytes32::new([0; 32]), cat_puzzle_hash.into(), 69); + let coin = Coin::new(parent_coin.coin_id(), cat_puzzle_hash.into(), 42); + + let inner_spend = StandardSpend::new() + .condition(ctx.alloc(CreateCoinWithoutMemos { + puzzle_hash: coin.puzzle_hash, + amount: coin.amount, + })?) + .inner_spend(&mut ctx, synthetic_key)?; + + let lineage_proof = LineageProof { + parent_parent_coin_id: parent_coin.parent_coin_info, + parent_inner_puzzle_hash: p2_puzzle_hash.into(), + parent_amount: parent_coin.amount, + }; + + CatSpend::new(asset_id) + .spend(coin, inner_spend, lineage_proof, 0) + .finish(&mut ctx)?; + + let coin_spend = ctx.take_spends().remove(0); + + let output_ptr = coin_spend + .puzzle_reveal + .run(&mut allocator, 0, u64::MAX, &coin_spend.solution)? + .1; + let actual = node_to_bytes(&allocator, output_ptr)?; + + let expected = hex!( + " + ffff46ffa06438c882c2db9f5c2a8b4cbda9258c40a6583b2d7c6becc1678607 + 4d558c834980ffff3cffa1cb9c4d253a0e1a091d620a55616e104f3329f58ee8 + 6e708d0527b1cc58a73b649e80ffff3dffa0c3bb7f0a7e1bd2cae332bbd0d1a7 + e275c1e6c643b2659e22c24f513886d3874e80ffff32ffb08584adae5630842a + 1766bc444d2b872dd3080f4e5daaecf6f762a4be7dc148f37868149d4217f3dc + c9183fe61e48d8bfffa0e5924c23faf33c9a1bf18c70d40cb09e4b194f521b9f + 6fceb2685c0612ac34a980ffff33ffa0f9f2d59294f2aae8f9833db876d1bf43 + 95d46af18c17312041c6f4a4d73fa041ff2a8080 + " + ); + assert_eq!(hex::encode(actual), hex::encode(expected)); + + Ok(()) + } + + #[test] + fn test_cat_spend_multi() -> anyhow::Result<()> { + let synthetic_key = + master_to_wallet_unhardened(&SECRET_KEY.public_key(), 0).derive_synthetic(); + + let mut allocator = Allocator::new(); + let mut ctx = SpendContext::new(&mut allocator); + + let asset_id = Bytes32::new([42; 32]); + + let p2_puzzle_hash = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { synthetic_key }, + } + .tree_hash(); + + let cat_puzzle_hash = CurriedProgram { + program: CAT_PUZZLE_HASH, + args: CatArgs { + mod_hash: CAT_PUZZLE_HASH.into(), + tail_program_hash: asset_id, + inner_puzzle: p2_puzzle_hash, + }, + } + .tree_hash() + .into(); + + let parent_coin_1 = Coin::new(Bytes32::new([0; 32]), cat_puzzle_hash, 69); + let coin_1 = Coin::new(parent_coin_1.coin_id(), cat_puzzle_hash, 42); + + let parent_coin_2 = Coin::new(Bytes32::new([0; 32]), cat_puzzle_hash, 69); + let coin_2 = Coin::new(parent_coin_2.coin_id(), cat_puzzle_hash, 34); + + let parent_coin_3 = Coin::new(Bytes32::new([0; 32]), cat_puzzle_hash, 69); + let coin_3 = Coin::new(parent_coin_3.coin_id(), cat_puzzle_hash, 69); + + let lineage_1 = LineageProof { + parent_parent_coin_id: parent_coin_1.parent_coin_info, + parent_inner_puzzle_hash: p2_puzzle_hash.into(), + parent_amount: parent_coin_1.amount, + }; + + let lineage_2 = LineageProof { + parent_parent_coin_id: parent_coin_2.parent_coin_info, + parent_inner_puzzle_hash: p2_puzzle_hash.into(), + parent_amount: parent_coin_2.amount, + }; + + let lineage_3 = LineageProof { + parent_parent_coin_id: parent_coin_3.parent_coin_info, + parent_inner_puzzle_hash: p2_puzzle_hash.into(), + parent_amount: parent_coin_3.amount, + }; + + let inner_spend = StandardSpend::new() + .condition(ctx.alloc(CreateCoinWithoutMemos { + puzzle_hash: coin_1.puzzle_hash, + amount: coin_1.amount + coin_2.amount + coin_3.amount, + })?) + .inner_spend(&mut ctx, synthetic_key)?; + + let empty_spend = StandardSpend::new().inner_spend(&mut ctx, synthetic_key)?; + + CatSpend::new(asset_id) + .spend(coin_1, inner_spend, lineage_1, 0) + .spend(coin_2, empty_spend, lineage_2, 0) + .spend(coin_3, empty_spend, lineage_3, 0) + .finish(&mut ctx)?; + + let coin_spends = ctx.take_spends(); + + let spend_vec = coin_spends + .clone() + .into_iter() + .map(|coin_spend| { + ( + coin_spend.coin, + coin_spend.puzzle_reveal, + coin_spend.solution, + ) + }) + .collect::>(); + let gen = solution_generator(spend_vec).unwrap(); + let block = + run_block_generator::(&mut allocator, &gen, &[], u64::MAX, 0) + .unwrap(); + + assert_eq!(block.cost, 101289468); + + assert_eq!(coin_spends.len(), 3); + + let output_ptr_1 = coin_spends[0] + .puzzle_reveal + .run(&mut allocator, 0, u64::MAX, &coin_spends[0].solution) + .unwrap() + .1; + let actual = node_to_bytes(&allocator, output_ptr_1).unwrap(); + + let expected = hex!( + " + ffff46ffa06438c882c2db9f5c2a8b4cbda9258c40a6583b2d7c6becc1678607 + 4d558c834980ffff3cffa1cb1cb6597fe61e67a6cbbcd4e8f0bda5e9fc56cd84 + c9e9502772b410dc8a03207680ffff3dffa0742ddb368882193072ea013bde24 + 4a5c9d40ab4454c09666e84777a79307e17a80ffff32ffb08584adae5630842a + 1766bc444d2b872dd3080f4e5daaecf6f762a4be7dc148f37868149d4217f3dc + c9183fe61e48d8bfffa004c476adfcffeacfef7c979bdd03b4641f1870d3f81b + 20636eefbcf879bb64ec80ffff33ffa0f9f2d59294f2aae8f9833db876d1bf43 + 95d46af18c17312041c6f4a4d73fa041ff8200918080 + " + ); + assert_eq!(hex::encode(actual), hex::encode(expected)); + + let output_ptr_2 = coin_spends[1] + .puzzle_reveal + .run(&mut allocator, 0, u64::MAX, &coin_spends[1].solution) + .unwrap() + .1; + let actual = node_to_bytes(&allocator, output_ptr_2).unwrap(); + + let expected = hex!( + " + ffff46ffa0ae60b8db0664959078a1c6e51ca6a8fc55207c63a8ac74d026f1d9 + 15c406bac480ffff3cffa1cb9a41843ab318a8336f61a6bf9e8b0b1d555b9f07 + cd19582e0bc52a961c65dc9e80ffff3dffa0294cda8d35164e01c4e3b7c07c36 + a5bb2f38a23e93ef49c882ee74349a0df8bd80ffff32ffb08584adae5630842a + 1766bc444d2b872dd3080f4e5daaecf6f762a4be7dc148f37868149d4217f3dc + c9183fe61e48d8bfffa0ba4484b961b7a2369d948d06c55b64bdbfaffb326bc1 + 3b490ab1215dd33d8d468080 + " + ); + assert_eq!(hex::encode(actual), hex::encode(expected)); + + let output_ptr_3 = coin_spends[2] + .puzzle_reveal + .run(&mut allocator, 0, u64::MAX, &coin_spends[2].solution) + .unwrap() + .1; + let actual = node_to_bytes(&allocator, output_ptr_3).unwrap(); + + let expected = hex!( + " + ffff46ffa0f8eacbef2bad0c7b27b638a90a37244e75013e977f250230856d05 + a2784e1d0980ffff3cffa1cb17c47c5fa8d795efa0d9227d2066cde36dd4e845 + 7e8f4e507d2015a1c7f3d94b80ffff3dffa0629abc502829339c7880ee003c4e + 68a8181d71206e50e7b36c29301ef60128f580ffff32ffb08584adae5630842a + 1766bc444d2b872dd3080f4e5daaecf6f762a4be7dc148f37868149d4217f3dc + c9183fe61e48d8bfffa0ba4484b961b7a2369d948d06c55b64bdbfaffb326bc1 + 3b490ab1215dd33d8d468080 + " + ); + assert_eq!(hex::encode(actual), hex::encode(expected)); + + Ok(()) + } +} diff --git a/src/spends/puzzles/cat/issue_cat.rs b/src/spends/puzzles/cat/issue_cat.rs new file mode 100644 index 00000000..812a4710 --- /dev/null +++ b/src/spends/puzzles/cat/issue_cat.rs @@ -0,0 +1,192 @@ +use chia_bls::PublicKey; +use chia_protocol::{Bytes32, Coin, CoinSpend}; +use chia_puzzles::cat::{ + CatArgs, CatSolution, CoinProof, EverythingWithSignatureTailArgs, CAT_PUZZLE_HASH, +}; +use clvm_traits::clvm_quote; +use clvm_utils::CurriedProgram; +use clvmr::NodePtr; + +use crate::{ChainedSpend, CreateCoinWithMemos, RunTail, SpendContext, SpendError}; + +pub struct IssueCat { + parent_coin_id: Bytes32, + conditions: Vec, +} + +pub struct CatIssuanceInfo { + pub asset_id: Bytes32, + pub eve_coin: Coin, + pub eve_inner_puzzle_hash: Bytes32, +} + +impl IssueCat { + pub fn new(parent_coin_id: Bytes32) -> Self { + Self { + parent_coin_id, + conditions: Vec::new(), + } + } + + pub fn condition(mut self, condition: NodePtr) -> Self { + self.conditions.push(condition); + self + } + + pub fn conditions(mut self, conditions: impl IntoIterator) -> Self { + self.conditions.extend(conditions); + self + } + + pub fn multi_issuance( + self, + ctx: &mut SpendContext, + public_key: PublicKey, + amount: u64, + ) -> Result<(ChainedSpend, CatIssuanceInfo), SpendError> { + let tail_puzzle_ptr = ctx.everything_with_signature_tail_puzzle(); + + let tail = ctx.alloc(CurriedProgram { + program: tail_puzzle_ptr, + args: EverythingWithSignatureTailArgs { public_key }, + })?; + let asset_id = ctx.tree_hash(tail).into(); + + self.condition(ctx.alloc(RunTail { + program: tail, + solution: NodePtr::NIL, + })?) + .finish(ctx, asset_id, amount) + } + + pub fn finish( + self, + ctx: &mut SpendContext, + asset_id: Bytes32, + amount: u64, + ) -> Result<(ChainedSpend, CatIssuanceInfo), SpendError> { + let cat_puzzle_ptr = ctx.cat_puzzle(); + + let inner_puzzle = ctx.alloc(clvm_quote!(self.conditions))?; + let inner_puzzle_hash = ctx.tree_hash(inner_puzzle).into(); + + let puzzle = ctx.alloc(CurriedProgram { + program: cat_puzzle_ptr, + args: CatArgs { + mod_hash: CAT_PUZZLE_HASH.into(), + tail_program_hash: asset_id, + inner_puzzle, + }, + })?; + + let puzzle_hash = ctx.tree_hash(puzzle).into(); + let coin = Coin::new(self.parent_coin_id, puzzle_hash, amount); + + let solution = ctx.serialize(CatSolution { + inner_puzzle_solution: (), + lineage_proof: None, + prev_coin_id: coin.coin_id(), + this_coin_info: coin, + next_coin_proof: CoinProof { + parent_coin_info: self.parent_coin_id, + inner_puzzle_hash, + amount, + }, + prev_subtotal: 0, + extra_delta: 0, + })?; + + let puzzle_reveal = ctx.serialize(puzzle)?; + ctx.spend(CoinSpend::new(coin, puzzle_reveal, solution)); + + let chained_spend = ChainedSpend { + parent_conditions: vec![ctx.alloc(CreateCoinWithMemos { + puzzle_hash, + amount, + memos: vec![puzzle_hash.to_vec().into()], + })?], + }; + + let issuance_info = CatIssuanceInfo { + asset_id, + eve_coin: coin, + eve_inner_puzzle_hash: inner_puzzle_hash, + }; + + Ok((chained_spend, issuance_info)) + } +} + +#[cfg(test)] +mod tests { + use chia_bls::{sign, Signature}; + use chia_protocol::SpendBundle; + use chia_puzzles::{ + standard::{StandardArgs, STANDARD_PUZZLE_HASH}, + DeriveSynthetic, + }; + use clvm_utils::ToTreeHash; + use clvmr::Allocator; + + use crate::{ + testing::SECRET_KEY, Chainable, CreateCoinWithMemos, RequiredSignature, StandardSpend, + WalletSimulator, + }; + + use super::*; + + #[tokio::test] + async fn test_cat_issuance() -> anyhow::Result<()> { + let sim = WalletSimulator::new().await; + let peer = sim.peer().await; + + let mut allocator = Allocator::new(); + let mut ctx = SpendContext::new(&mut allocator); + + let sk = SECRET_KEY.derive_synthetic(); + let pk = sk.public_key(); + + let puzzle_hash = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { synthetic_key: pk }, + } + .tree_hash() + .into(); + + let xch_coin = sim.generate_coin(puzzle_hash, 1).await.coin; + + let (issue_cat, _cat_info) = IssueCat::new(xch_coin.coin_id()) + .condition(ctx.alloc(CreateCoinWithMemos { + puzzle_hash, + amount: 1, + memos: vec![puzzle_hash.to_vec().into()], + })?) + .multi_issuance(&mut ctx, pk, 1)?; + + StandardSpend::new() + .chain(issue_cat) + .finish(&mut ctx, xch_coin, pk)?; + + let coin_spends = ctx.take_spends(); + + let required_signatures = RequiredSignature::from_coin_spends( + &mut allocator, + &coin_spends, + WalletSimulator::AGG_SIG_ME.into(), + )?; + + let mut aggregated_signature = Signature::default(); + + for required in required_signatures { + aggregated_signature += &sign(&sk, required.final_message()); + } + + let spend_bundle = SpendBundle::new(coin_spends, aggregated_signature); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.error, None); + assert_eq!(ack.status, 1); + + Ok(()) + } +} diff --git a/src/spends/puzzles/did.rs b/src/spends/puzzles/did.rs new file mode 100644 index 00000000..469f2cb7 --- /dev/null +++ b/src/spends/puzzles/did.rs @@ -0,0 +1,83 @@ +mod create_did; +mod did_info; +mod did_spend; + +pub use create_did::*; +pub use did_info::*; +pub use did_spend::*; + +#[cfg(test)] +mod tests { + use super::*; + + use chia_bls::{sign, Signature}; + use chia_protocol::SpendBundle; + use chia_puzzles::{ + standard::{StandardArgs, STANDARD_PUZZLE_HASH}, + DeriveSynthetic, + }; + use clvm_utils::{CurriedProgram, ToTreeHash}; + use clvmr::Allocator; + + use crate::{ + testing::SECRET_KEY, Chainable, Launcher, RequiredSignature, SpendContext, StandardSpend, + WalletSimulator, + }; + + #[tokio::test] + async fn test_create_did() -> anyhow::Result<()> { + let sim = WalletSimulator::new().await; + let peer = sim.peer().await; + + let sk = SECRET_KEY.derive_synthetic(); + let pk = sk.public_key(); + + let puzzle_hash = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { synthetic_key: pk }, + } + .tree_hash() + .into(); + + let parent = sim.generate_coin(puzzle_hash, 1).await.coin; + + let mut allocator = Allocator::new(); + let mut ctx = SpendContext::new(&mut allocator); + + let (launch_singleton, _did_info) = Launcher::new(parent.coin_id(), 1) + .create(&mut ctx)? + .create_standard_did(&mut ctx, pk)?; + + StandardSpend::new() + .chain(launch_singleton) + .finish(&mut ctx, parent, pk)?; + + let coin_spends = ctx.take_spends(); + + let mut spend_bundle = SpendBundle::new(coin_spends, Signature::default()); + + let required_signatures = RequiredSignature::from_coin_spends( + &mut allocator, + &spend_bundle.coin_spends, + WalletSimulator::AGG_SIG_ME.into(), + ) + .unwrap(); + + for required in required_signatures { + spend_bundle.aggregated_signature += &sign(&sk, required.final_message()); + } + + let ack = peer.send_transaction(spend_bundle).await.unwrap(); + assert_eq!(ack.error, None); + assert_eq!(ack.status, 1); + + // Make sure the DID was created. + let found_coins = peer + .register_for_ph_updates(vec![puzzle_hash], 0) + .await + .unwrap(); + assert_eq!(found_coins.len(), 2); + + Ok(()) + } +} diff --git a/src/spends/puzzles/did/create_did.rs b/src/spends/puzzles/did/create_did.rs new file mode 100644 index 00000000..b2ceefe4 --- /dev/null +++ b/src/spends/puzzles/did/create_did.rs @@ -0,0 +1,210 @@ +use chia_bls::PublicKey; +use chia_protocol::Bytes32; +use chia_puzzles::{ + did::DID_INNER_PUZZLE_HASH, + singleton::{SINGLETON_LAUNCHER_PUZZLE_HASH, SINGLETON_TOP_LAYER_PUZZLE_HASH}, + standard::{StandardArgs, STANDARD_PUZZLE_HASH}, + EveProof, Proof, +}; +use clvm_traits::ToClvm; +use clvm_utils::{curry_tree_hash, tree_hash_atom, tree_hash_pair, CurriedProgram, ToTreeHash}; +use clvmr::NodePtr; + +use crate::{ + u64_to_bytes, ChainedSpend, DidInfo, SpendContext, SpendError, SpendableLauncher, + StandardDidSpend, +}; + +pub trait CreateDid { + fn create_eve_did( + self, + ctx: &mut SpendContext, + inner_puzzle_hash: Bytes32, + recovery_did_list_hash: Bytes32, + num_verifications_required: u64, + metadata: M, + ) -> Result<(ChainedSpend, DidInfo), SpendError> + where + M: ToClvm; + + fn create_custom_standard_did( + self, + ctx: &mut SpendContext, + recovery_did_list_hash: Bytes32, + num_verifications_required: u64, + metadata: M, + synthetic_key: PublicKey, + ) -> Result<(ChainedSpend, DidInfo), SpendError> + where + M: ToClvm, + Self: Sized, + { + let inner_puzzle_hash = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { synthetic_key }, + } + .tree_hash() + .into(); + + let (create_did, did_info) = self.create_eve_did( + ctx, + inner_puzzle_hash, + recovery_did_list_hash, + num_verifications_required, + metadata, + )?; + + let did_info = StandardDidSpend::new() + .recreate() + .finish(ctx, synthetic_key, did_info)?; + + Ok((create_did, did_info)) + } + + fn create_standard_did( + self, + ctx: &mut SpendContext, + synthetic_key: PublicKey, + ) -> Result<(ChainedSpend, DidInfo<()>), SpendError> + where + Self: Sized, + { + self.create_custom_standard_did(ctx, tree_hash_atom(&[]).into(), 1, (), synthetic_key) + } +} + +impl CreateDid for SpendableLauncher { + fn create_eve_did( + self, + ctx: &mut SpendContext, + p2_puzzle_hash: Bytes32, + recovery_did_list_hash: Bytes32, + num_verifications_required: u64, + metadata: M, + ) -> Result<(ChainedSpend, DidInfo), SpendError> + where + M: ToClvm, + { + let metadata_ptr = ctx.alloc(&metadata)?; + let metadata_hash = ctx.tree_hash(metadata_ptr).into(); + + let did_inner_puzzle_hash = did_inner_puzzle_hash( + p2_puzzle_hash, + recovery_did_list_hash, + num_verifications_required, + self.coin().coin_id(), + metadata_hash, + ); + + let launcher_coin = self.coin(); + let (chained_spend, eve_coin) = self.spend(ctx, did_inner_puzzle_hash, ())?; + + let proof = Proof::Eve(EveProof { + parent_coin_info: launcher_coin.parent_coin_info, + amount: launcher_coin.amount, + }); + + let did_info = DidInfo { + launcher_id: launcher_coin.coin_id(), + coin: eve_coin, + did_inner_puzzle_hash, + p2_puzzle_hash, + proof, + recovery_did_list_hash, + num_verifications_required, + metadata, + }; + + Ok((chained_spend, did_info)) + } +} + +pub fn did_inner_puzzle_hash( + inner_puzzle_hash: Bytes32, + recovery_did_list_hash: Bytes32, + num_verifications_required: u64, + launcher_id: Bytes32, + metadata_hash: Bytes32, +) -> Bytes32 { + let recovery_hash = tree_hash_atom(&recovery_did_list_hash); + let num_verifications_hash = tree_hash_atom(&u64_to_bytes(num_verifications_required)); + + let singleton_hash = tree_hash_atom(&SINGLETON_TOP_LAYER_PUZZLE_HASH); + let launcher_id_hash = tree_hash_atom(&launcher_id); + let launcher_puzzle_hash = tree_hash_atom(&SINGLETON_LAUNCHER_PUZZLE_HASH); + + let pair = tree_hash_pair(launcher_id_hash, launcher_puzzle_hash); + let singleton_struct_hash = tree_hash_pair(singleton_hash, pair); + + curry_tree_hash( + DID_INNER_PUZZLE_HASH, + &[ + inner_puzzle_hash.into(), + recovery_hash, + num_verifications_hash, + singleton_struct_hash, + metadata_hash.into(), + ], + ) + .into() +} + +#[cfg(test)] +mod tests { + use super::*; + + use chia_puzzles::{ + did::DidArgs, + singleton::{ + SingletonStruct, SINGLETON_LAUNCHER_PUZZLE_HASH, SINGLETON_TOP_LAYER_PUZZLE_HASH, + }, + }; + use clvm_utils::CurriedProgram; + use clvmr::Allocator; + + #[test] + fn test_puzzle_hash() { + let mut allocator = Allocator::new(); + let mut ctx = SpendContext::new(&mut allocator); + + let inner_puzzle = ctx.alloc([1, 2, 3]).unwrap(); + let inner_puzzle_hash = ctx.tree_hash(inner_puzzle).into(); + + let metadata = ctx.alloc([4, 5, 6]).unwrap(); + let metadata_hash = ctx.tree_hash(metadata).into(); + + let launcher_id = Bytes32::new([34; 32]); + let recovery_did_list_hash = Bytes32::new([42; 32]); + let num_verifications_required = 2; + + let did_inner_puzzle = ctx.did_inner_puzzle(); + + let puzzle = ctx + .alloc(CurriedProgram { + program: did_inner_puzzle, + args: DidArgs { + inner_puzzle, + recovery_did_list_hash, + num_verifications_required, + singleton_struct: SingletonStruct { + mod_hash: SINGLETON_TOP_LAYER_PUZZLE_HASH.into(), + launcher_id, + launcher_puzzle_hash: SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + }, + metadata, + }, + }) + .unwrap(); + let allocated_puzzle_hash = ctx.tree_hash(puzzle); + + let puzzle_hash = did_inner_puzzle_hash( + inner_puzzle_hash, + recovery_did_list_hash, + num_verifications_required, + launcher_id, + metadata_hash, + ); + + assert_eq!(hex::encode(allocated_puzzle_hash), hex::encode(puzzle_hash)); + } +} diff --git a/src/spends/puzzles/did/did_info.rs b/src/spends/puzzles/did/did_info.rs new file mode 100644 index 00000000..2ec279be --- /dev/null +++ b/src/spends/puzzles/did/did_info.rs @@ -0,0 +1,29 @@ +use chia_protocol::{Bytes32, Coin}; +use chia_puzzles::Proof; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DidInfo { + pub launcher_id: Bytes32, + pub coin: Coin, + pub did_inner_puzzle_hash: Bytes32, + pub p2_puzzle_hash: Bytes32, + pub proof: Proof, + pub recovery_did_list_hash: Bytes32, + pub num_verifications_required: u64, + pub metadata: M, +} + +impl DidInfo { + pub fn with_metadata(self, metadata: N) -> DidInfo { + DidInfo { + launcher_id: self.launcher_id, + coin: self.coin, + did_inner_puzzle_hash: self.did_inner_puzzle_hash, + p2_puzzle_hash: self.p2_puzzle_hash, + proof: self.proof, + recovery_did_list_hash: self.recovery_did_list_hash, + num_verifications_required: self.num_verifications_required, + metadata, + } + } +} diff --git a/src/spends/puzzles/did/did_spend.rs b/src/spends/puzzles/did/did_spend.rs new file mode 100644 index 00000000..22f1e14e --- /dev/null +++ b/src/spends/puzzles/did/did_spend.rs @@ -0,0 +1,223 @@ +use chia_bls::PublicKey; +use chia_protocol::{Coin, CoinSpend}; +use chia_puzzles::{ + did::{DidArgs, DidSolution}, + singleton::{SingletonStruct, SINGLETON_LAUNCHER_PUZZLE_HASH, SINGLETON_TOP_LAYER_PUZZLE_HASH}, + LineageProof, Proof, +}; +use clvm_traits::ToClvm; +use clvm_utils::CurriedProgram; +use clvmr::NodePtr; + +use crate::{ + spend_singleton, Chainable, ChainedSpend, CreateCoinWithMemos, DidInfo, InnerSpend, + SpendContext, SpendError, StandardSpend, +}; + +pub struct NoDidOutput; + +pub enum DidOutput { + Recreate, +} + +pub struct StandardDidSpend { + standard_spend: StandardSpend, + output: T, +} + +impl Default for StandardDidSpend { + fn default() -> Self { + Self { + output: NoDidOutput, + standard_spend: StandardSpend::new(), + } + } +} + +impl StandardDidSpend { + pub fn new() -> Self { + Self::default() + } + + pub fn recreate(self) -> StandardDidSpend { + StandardDidSpend { + standard_spend: self.standard_spend, + output: DidOutput::Recreate, + } + } +} + +impl StandardDidSpend { + pub fn finish( + self, + ctx: &mut SpendContext, + synthetic_key: PublicKey, + mut did_info: DidInfo, + ) -> Result, SpendError> + where + M: ToClvm, + { + let create_coin = match self.output { + DidOutput::Recreate => CreateCoinWithMemos { + puzzle_hash: did_info.did_inner_puzzle_hash, + amount: did_info.coin.amount, + memos: vec![did_info.p2_puzzle_hash.to_vec().into()], + }, + }; + + let inner_spend = self + .standard_spend + .condition(ctx.alloc(create_coin)?) + .inner_spend(ctx, synthetic_key)?; + + let did_spend = raw_did_spend(ctx, &did_info, inner_spend)?; + ctx.spend(did_spend); + + match self.output { + DidOutput::Recreate => { + did_info.proof = Proof::Lineage(LineageProof { + parent_parent_coin_id: did_info.coin.parent_coin_info, + parent_inner_puzzle_hash: did_info.did_inner_puzzle_hash, + parent_amount: did_info.coin.amount, + }); + + did_info.coin = Coin::new( + did_info.coin.coin_id(), + did_info.coin.puzzle_hash, + did_info.coin.amount, + ); + } + } + + Ok(did_info) + } +} + +impl Chainable for StandardDidSpend { + fn chain(mut self, chained_spend: ChainedSpend) -> Self { + self.standard_spend = self.standard_spend.chain(chained_spend); + self + } + + fn condition(mut self, condition: NodePtr) -> Self { + self.standard_spend = self.standard_spend.condition(condition); + self + } +} + +pub fn raw_did_spend( + ctx: &mut SpendContext, + did_info: &DidInfo, + inner_spend: InnerSpend, +) -> Result +where + M: ToClvm, +{ + let did_inner_puzzle = ctx.did_inner_puzzle(); + + let puzzle = ctx.alloc(CurriedProgram { + program: did_inner_puzzle, + args: DidArgs { + inner_puzzle: inner_spend.puzzle(), + recovery_did_list_hash: did_info.recovery_did_list_hash, + num_verifications_required: did_info.num_verifications_required, + singleton_struct: SingletonStruct { + mod_hash: SINGLETON_TOP_LAYER_PUZZLE_HASH.into(), + launcher_id: did_info.launcher_id, + launcher_puzzle_hash: SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + }, + metadata: &did_info.metadata, + }, + })?; + + let solution = ctx.alloc(DidSolution::InnerSpend(inner_spend.solution()))?; + + let did_spend = InnerSpend::new(puzzle, solution); + + spend_singleton( + ctx, + did_info.coin, + did_info.launcher_id, + did_info.proof, + did_spend, + ) +} + +#[cfg(test)] +mod tests { + use chia_bls::{sign, Signature}; + use chia_protocol::SpendBundle; + use chia_puzzles::{ + standard::{StandardArgs, STANDARD_PUZZLE_HASH}, + DeriveSynthetic, + }; + use clvm_utils::ToTreeHash; + use clvmr::Allocator; + + use crate::{testing::SECRET_KEY, CreateDid, Launcher, RequiredSignature, WalletSimulator}; + + use super::*; + + #[tokio::test] + async fn test_did_recreation() -> anyhow::Result<()> { + let sim = WalletSimulator::new().await; + let peer = sim.peer().await; + + let mut allocator = Allocator::new(); + let mut ctx = SpendContext::new(&mut allocator); + + let sk = SECRET_KEY.derive_synthetic(); + let pk = sk.public_key(); + + let puzzle_hash = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { synthetic_key: pk }, + } + .tree_hash() + .into(); + + let parent = sim.generate_coin(puzzle_hash, 1).await.coin; + + let (create_did, mut did_info) = Launcher::new(parent.coin_id(), 1) + .create(&mut ctx)? + .create_standard_did(&mut ctx, pk)?; + + StandardSpend::new() + .chain(create_did) + .finish(&mut ctx, parent, pk)?; + + for _ in 0..10 { + did_info = StandardDidSpend::new() + .recreate() + .finish(&mut ctx, pk, did_info)?; + } + + let coin_spends = ctx.take_spends(); + + let required_signatures = RequiredSignature::from_coin_spends( + &mut allocator, + &coin_spends, + WalletSimulator::AGG_SIG_ME.into(), + )?; + + let mut aggregated_signature = Signature::default(); + + for required in required_signatures { + aggregated_signature += &sign(&sk, required.final_message()); + } + + let ack = peer + .send_transaction(SpendBundle::new(coin_spends, aggregated_signature)) + .await?; + assert_eq!(ack.error, None); + assert_eq!(ack.status, 1); + + let coin_state = peer + .register_for_coin_updates(vec![did_info.coin.coin_id()], 0) + .await? + .remove(0); + assert_eq!(coin_state.coin, did_info.coin); + + Ok(()) + } +} diff --git a/src/spends/puzzles/nft.rs b/src/spends/puzzles/nft.rs new file mode 100644 index 00000000..06503bf9 --- /dev/null +++ b/src/spends/puzzles/nft.rs @@ -0,0 +1,7 @@ +mod mint_nft; +mod nft_info; +mod nft_spend; + +pub use mint_nft::*; +pub use nft_info::*; +pub use nft_spend::*; diff --git a/src/spends/puzzles/nft/mint_nft.rs b/src/spends/puzzles/nft/mint_nft.rs new file mode 100644 index 00000000..1ac1b1f1 --- /dev/null +++ b/src/spends/puzzles/nft/mint_nft.rs @@ -0,0 +1,250 @@ +use chia_bls::PublicKey; +use chia_protocol::Bytes32; +use chia_puzzles::{ + nft::{ + NftOwnershipLayerArgs, NftRoyaltyTransferPuzzleArgs, NftStateLayerArgs, + NFT_METADATA_UPDATER_PUZZLE_HASH, NFT_OWNERSHIP_LAYER_PUZZLE_HASH, + NFT_ROYALTY_TRANSFER_PUZZLE_HASH, NFT_STATE_LAYER_PUZZLE_HASH, + }, + singleton::SingletonStruct, + standard::{StandardArgs, STANDARD_PUZZLE_HASH}, + EveProof, Proof, +}; +use clvm_traits::ToClvm; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::NodePtr; + +use crate::{ + ChainedSpend, CreatePuzzleAnnouncement, NftInfo, SpendContext, SpendError, SpendableLauncher, + StandardNftSpend, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct StandardMint { + pub metadata: M, + pub royalty_puzzle_hash: Bytes32, + pub royalty_percentage: u16, + pub synthetic_key: PublicKey, + pub owner_puzzle_hash: Bytes32, + pub did_id: Bytes32, + pub did_inner_puzzle_hash: Bytes32, +} + +pub trait MintNft { + fn mint_eve_nft( + self, + ctx: &mut SpendContext, + inner_puzzle_hash: Bytes32, + metadata: M, + royalty_puzzle_hash: Bytes32, + royalty_percentage: u16, + ) -> Result<(ChainedSpend, NftInfo), SpendError> + where + M: ToClvm; + + fn mint_standard_nft( + self, + ctx: &mut SpendContext, + mint: StandardMint, + ) -> Result<(ChainedSpend, NftInfo), SpendError> + where + M: ToClvm, + Self: Sized, + { + let inner_puzzle_hash = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { + synthetic_key: mint.synthetic_key, + }, + } + .tree_hash() + .into(); + + let (mut mint_nft, nft_info) = self.mint_eve_nft( + ctx, + inner_puzzle_hash, + mint.metadata, + mint.royalty_puzzle_hash, + mint.royalty_percentage, + )?; + + let (nft_spend, nft_info) = StandardNftSpend::new() + .new_owner(mint.did_id, mint.did_inner_puzzle_hash) + .transfer(mint.owner_puzzle_hash) + .finish(ctx, mint.synthetic_key, nft_info)?; + + mint_nft.extend(nft_spend); + + Ok((mint_nft, nft_info)) + } +} + +impl MintNft for SpendableLauncher { + fn mint_eve_nft( + self, + ctx: &mut SpendContext, + p2_puzzle_hash: Bytes32, + metadata: M, + royalty_puzzle_hash: Bytes32, + royalty_percentage: u16, + ) -> Result<(ChainedSpend, NftInfo), SpendError> + where + M: ToClvm, + { + let metadata_ptr = ctx.alloc(&metadata)?; + let metadata_hash = ctx.tree_hash(metadata_ptr); + + let transfer_program = CurriedProgram { + program: NFT_ROYALTY_TRANSFER_PUZZLE_HASH, + args: NftRoyaltyTransferPuzzleArgs { + singleton_struct: SingletonStruct::new(self.coin().coin_id()), + royalty_puzzle_hash, + trade_price_percentage: royalty_percentage, + }, + }; + + let ownership_layer = CurriedProgram { + program: NFT_OWNERSHIP_LAYER_PUZZLE_HASH, + args: NftOwnershipLayerArgs { + mod_hash: NFT_OWNERSHIP_LAYER_PUZZLE_HASH.into(), + current_owner: None, + transfer_program, + inner_puzzle: TreeHash::from(p2_puzzle_hash), + }, + }; + + let nft_inner_puzzle_hash = CurriedProgram { + program: NFT_STATE_LAYER_PUZZLE_HASH, + args: NftStateLayerArgs { + mod_hash: NFT_STATE_LAYER_PUZZLE_HASH.into(), + metadata: metadata_hash, + metadata_updater_puzzle_hash: NFT_METADATA_UPDATER_PUZZLE_HASH.into(), + inner_puzzle: ownership_layer, + }, + } + .tree_hash() + .into(); + + let launcher_coin = self.coin(); + let (mut chained_spend, eve_coin) = self.spend(ctx, nft_inner_puzzle_hash, ())?; + + chained_spend + .parent_conditions + .push(ctx.alloc(CreatePuzzleAnnouncement { + message: launcher_coin.coin_id().to_vec().into(), + })?); + + let proof = Proof::Eve(EveProof { + parent_coin_info: launcher_coin.parent_coin_info, + amount: launcher_coin.amount, + }); + + let nft_info = NftInfo { + launcher_id: launcher_coin.coin_id(), + coin: eve_coin, + nft_inner_puzzle_hash, + p2_puzzle_hash, + proof, + metadata, + metadata_updater_hash: NFT_METADATA_UPDATER_PUZZLE_HASH.into(), + current_owner: None, + royalty_puzzle_hash, + royalty_percentage, + }; + + Ok((chained_spend, nft_info)) + } +} + +#[cfg(test)] +mod tests { + use chia_bls::{sign, Signature}; + use chia_protocol::SpendBundle; + use chia_puzzles::DeriveSynthetic; + use clvm_utils::CurriedProgram; + use clvmr::Allocator; + + use crate::{ + testing::SECRET_KEY, Chainable, CreateDid, IntermediateLauncher, Launcher, + RequiredSignature, StandardDidSpend, StandardSpend, WalletSimulator, + }; + + use super::*; + + #[tokio::test] + async fn test_bulk_mint() -> anyhow::Result<()> { + let sim = WalletSimulator::new().await; + let peer = sim.peer().await; + + let mut allocator = Allocator::new(); + let mut ctx = SpendContext::new(&mut allocator); + + let sk = SECRET_KEY.derive_synthetic(); + let pk = sk.public_key(); + + let puzzle_hash = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { synthetic_key: pk }, + } + .tree_hash() + .into(); + + let parent = sim.generate_coin(puzzle_hash, 3).await.coin; + + let (create_did, did_info) = Launcher::new(parent.coin_id(), 1) + .create(&mut ctx)? + .create_standard_did(&mut ctx, pk)?; + + StandardSpend::new() + .chain(create_did) + .finish(&mut ctx, parent, pk)?; + + let mint = StandardMint { + metadata: (), + royalty_puzzle_hash: puzzle_hash, + royalty_percentage: 100, + owner_puzzle_hash: puzzle_hash, + synthetic_key: pk, + did_id: did_info.launcher_id, + did_inner_puzzle_hash: did_info.did_inner_puzzle_hash, + }; + + let _did_info = StandardDidSpend::new() + .chain( + IntermediateLauncher::new(did_info.coin.coin_id(), 0, 2) + .create(&mut ctx)? + .mint_standard_nft(&mut ctx, mint.clone())? + .0, + ) + .chain( + IntermediateLauncher::new(did_info.coin.coin_id(), 1, 2) + .create(&mut ctx)? + .mint_standard_nft(&mut ctx, mint.clone())? + .0, + ) + .recreate() + .finish(&mut ctx, pk, did_info)?; + + let coin_spends = ctx.take_spends(); + + let required_signatures = RequiredSignature::from_coin_spends( + &mut allocator, + &coin_spends, + WalletSimulator::AGG_SIG_ME.into(), + )?; + + let mut aggregated_signature = Signature::default(); + + for required in required_signatures { + aggregated_signature += &sign(&sk, required.final_message()); + } + + let spend_bundle = SpendBundle::new(coin_spends, aggregated_signature); + let ack = peer.send_transaction(spend_bundle).await?; + + assert_eq!(ack.error, None); + assert_eq!(ack.status, 1); + + Ok(()) + } +} diff --git a/src/spends/puzzles/nft/nft_info.rs b/src/spends/puzzles/nft/nft_info.rs new file mode 100644 index 00000000..d6f73e33 --- /dev/null +++ b/src/spends/puzzles/nft/nft_info.rs @@ -0,0 +1,33 @@ +use chia_protocol::{Bytes32, Coin}; +use chia_puzzles::Proof; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NftInfo { + pub launcher_id: Bytes32, + pub coin: Coin, + pub nft_inner_puzzle_hash: Bytes32, + pub p2_puzzle_hash: Bytes32, + pub proof: Proof, + pub metadata: M, + pub metadata_updater_hash: Bytes32, + pub current_owner: Option, + pub royalty_puzzle_hash: Bytes32, + pub royalty_percentage: u16, +} + +impl NftInfo { + pub fn with_metadata(self, metadata: N) -> NftInfo { + NftInfo { + launcher_id: self.launcher_id, + coin: self.coin, + nft_inner_puzzle_hash: self.nft_inner_puzzle_hash, + p2_puzzle_hash: self.p2_puzzle_hash, + proof: self.proof, + metadata, + metadata_updater_hash: self.metadata_updater_hash, + current_owner: self.current_owner, + royalty_puzzle_hash: self.royalty_puzzle_hash, + royalty_percentage: self.royalty_percentage, + } + } +} diff --git a/src/spends/puzzles/nft/nft_spend.rs b/src/spends/puzzles/nft/nft_spend.rs new file mode 100644 index 00000000..d6c0b83b --- /dev/null +++ b/src/spends/puzzles/nft/nft_spend.rs @@ -0,0 +1,414 @@ +use chia_bls::PublicKey; +use chia_protocol::{Bytes32, Coin, CoinSpend}; +use chia_puzzles::{ + nft::{ + NftOwnershipLayerArgs, NftOwnershipLayerSolution, NftRoyaltyTransferPuzzleArgs, + NftStateLayerArgs, NftStateLayerSolution, NFT_OWNERSHIP_LAYER_PUZZLE_HASH, + NFT_ROYALTY_TRANSFER_PUZZLE_HASH, NFT_STATE_LAYER_PUZZLE_HASH, + }, + singleton::{SingletonStruct, SINGLETON_LAUNCHER_PUZZLE_HASH, SINGLETON_TOP_LAYER_PUZZLE_HASH}, + LineageProof, Proof, +}; +use clvm_traits::{clvm_list, ToClvm}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::NodePtr; +use sha2::{Digest, Sha256}; + +use crate::{ + singleton_puzzle_hash, spend_singleton, AssertPuzzleAnnouncement, Chainable, ChainedSpend, + CreateCoinWithMemos, InnerSpend, NewNftOwner, NftInfo, SpendContext, SpendError, StandardSpend, +}; + +pub struct NoNftOutput; + +pub enum NftOutput { + SamePuzzleHash, + NewPuzzleHash { puzzle_hash: Bytes32 }, +} + +pub struct StandardNftSpend { + standard_spend: StandardSpend, + output: T, + new_owner: Option, +} + +impl Default for StandardNftSpend { + fn default() -> Self { + Self { + output: NoNftOutput, + standard_spend: StandardSpend::new(), + new_owner: None, + } + } +} + +impl StandardNftSpend { + pub fn new() -> Self { + Self::default() + } + + pub fn update(self) -> StandardNftSpend { + StandardNftSpend { + standard_spend: self.standard_spend, + output: NftOutput::SamePuzzleHash, + new_owner: self.new_owner, + } + } + + pub fn transfer(self, puzzle_hash: Bytes32) -> StandardNftSpend { + StandardNftSpend { + standard_spend: self.standard_spend, + output: NftOutput::NewPuzzleHash { puzzle_hash }, + new_owner: self.new_owner, + } + } +} + +impl StandardNftSpend { + pub fn new_owner(mut self, did_id: Bytes32, did_inner_puzzle_hash: Bytes32) -> Self { + self.new_owner = Some(NewNftOwner { + new_owner: Some(did_id), + trade_prices_list: Vec::new(), + new_did_inner_hash: Some(did_inner_puzzle_hash), + }); + self + } +} + +impl StandardNftSpend { + pub fn finish( + mut self, + ctx: &mut SpendContext, + synthetic_key: PublicKey, + mut nft_info: NftInfo, + ) -> Result<(ChainedSpend, NftInfo), SpendError> + where + M: ToClvm, + { + let mut chained_spend = ChainedSpend { + parent_conditions: Vec::new(), + }; + + let p2_puzzle_hash = match self.output { + NftOutput::SamePuzzleHash => nft_info.p2_puzzle_hash, + NftOutput::NewPuzzleHash { puzzle_hash } => puzzle_hash, + }; + + if let Some(new_owner) = &self.new_owner { + self.standard_spend = self.standard_spend.condition(ctx.alloc(new_owner)?); + + let new_nft_owner_args = ctx.alloc(clvm_list!( + new_owner.new_owner, + new_owner.trade_prices_list.clone(), + new_owner.new_did_inner_hash + ))?; + + let mut announcement_id = Sha256::new(); + announcement_id.update(nft_info.coin.puzzle_hash); + announcement_id.update([0xad, 0x4c]); + announcement_id.update(ctx.tree_hash(new_nft_owner_args)); + + chained_spend + .parent_conditions + .push(ctx.alloc(AssertPuzzleAnnouncement { + announcement_id: Bytes32::new(announcement_id.finalize().into()), + })?); + } + + let inner_spend = self + .standard_spend + .condition(ctx.alloc(CreateCoinWithMemos { + puzzle_hash: p2_puzzle_hash, + amount: nft_info.coin.amount, + memos: vec![p2_puzzle_hash.to_vec().into()], + })?) + .inner_spend(ctx, synthetic_key)?; + + let nft_spend = raw_nft_spend(ctx, &nft_info, inner_spend)?; + ctx.spend(nft_spend); + + nft_info.current_owner = self + .new_owner + .map(|value| value.new_owner) + .unwrap_or(nft_info.current_owner); + + let metadata_ptr = ctx.alloc(&nft_info.metadata)?; + + let transfer_program = CurriedProgram { + program: NFT_ROYALTY_TRANSFER_PUZZLE_HASH, + args: NftRoyaltyTransferPuzzleArgs { + singleton_struct: SingletonStruct::new(nft_info.launcher_id), + royalty_puzzle_hash: nft_info.royalty_puzzle_hash, + trade_price_percentage: nft_info.royalty_percentage, + }, + }; + + let ownership_layer = CurriedProgram { + program: NFT_OWNERSHIP_LAYER_PUZZLE_HASH, + args: NftOwnershipLayerArgs { + mod_hash: NFT_OWNERSHIP_LAYER_PUZZLE_HASH.into(), + current_owner: nft_info.current_owner, + transfer_program, + inner_puzzle: TreeHash::from(p2_puzzle_hash), + }, + }; + + let new_inner_puzzle_hash = CurriedProgram { + program: NFT_STATE_LAYER_PUZZLE_HASH, + args: NftStateLayerArgs { + mod_hash: NFT_STATE_LAYER_PUZZLE_HASH.into(), + metadata: ctx.tree_hash(metadata_ptr), + metadata_updater_puzzle_hash: nft_info.metadata_updater_hash, + inner_puzzle: ownership_layer, + }, + } + .tree_hash() + .into(); + + let new_puzzle_hash = singleton_puzzle_hash(nft_info.launcher_id, new_inner_puzzle_hash); + + nft_info.proof = Proof::Lineage(LineageProof { + parent_parent_coin_id: nft_info.coin.parent_coin_info, + parent_inner_puzzle_hash: nft_info.nft_inner_puzzle_hash, + parent_amount: nft_info.coin.amount, + }); + + nft_info.coin = Coin::new( + nft_info.coin.coin_id(), + new_puzzle_hash, + nft_info.coin.amount, + ); + + nft_info.nft_inner_puzzle_hash = new_inner_puzzle_hash; + + Ok((chained_spend, nft_info)) + } +} + +impl Chainable for StandardNftSpend { + fn chain(mut self, chained_spend: ChainedSpend) -> Self { + self.standard_spend = self.standard_spend.chain(chained_spend); + self + } + + fn condition(mut self, condition: NodePtr) -> Self { + self.standard_spend = self.standard_spend.condition(condition); + self + } +} + +pub fn raw_nft_spend( + ctx: &mut SpendContext, + nft_info: &NftInfo, + inner_spend: InnerSpend, +) -> Result +where + M: ToClvm, +{ + let transfer_program_puzzle = ctx.nft_royalty_transfer(); + + let transfer_program = CurriedProgram { + program: transfer_program_puzzle, + args: NftRoyaltyTransferPuzzleArgs { + singleton_struct: SingletonStruct { + mod_hash: SINGLETON_TOP_LAYER_PUZZLE_HASH.into(), + launcher_id: nft_info.launcher_id, + launcher_puzzle_hash: SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + }, + royalty_puzzle_hash: nft_info.royalty_puzzle_hash, + trade_price_percentage: nft_info.royalty_percentage, + }, + }; + + let ownership_layer_spend = + spend_nft_ownership_layer(ctx, nft_info.current_owner, transfer_program, inner_spend)?; + + let state_layer_spend = spend_nft_state_layer( + ctx, + &nft_info.metadata, + nft_info.metadata_updater_hash, + ownership_layer_spend, + )?; + + spend_singleton( + ctx, + nft_info.coin, + nft_info.launcher_id, + nft_info.proof, + state_layer_spend, + ) +} + +pub fn spend_nft_state_layer( + ctx: &mut SpendContext, + metadata: M, + metadata_updater_puzzle_hash: Bytes32, + inner_spend: InnerSpend, +) -> Result +where + M: ToClvm, +{ + let nft_state_layer = ctx.nft_state_layer(); + + let puzzle = ctx.alloc(CurriedProgram { + program: nft_state_layer, + args: NftStateLayerArgs { + mod_hash: NFT_STATE_LAYER_PUZZLE_HASH.into(), + metadata, + metadata_updater_puzzle_hash, + inner_puzzle: inner_spend.puzzle(), + }, + })?; + + let solution = ctx.alloc(NftStateLayerSolution { + inner_solution: inner_spend.solution(), + })?; + + Ok(InnerSpend::new(puzzle, solution)) +} + +pub fn spend_nft_ownership_layer

( + ctx: &mut SpendContext, + current_owner: Option, + transfer_program: P, + inner_spend: InnerSpend, +) -> Result +where + P: ToClvm, +{ + let nft_ownership_layer = ctx.nft_ownership_layer(); + + let puzzle = ctx.alloc(CurriedProgram { + program: nft_ownership_layer, + args: NftOwnershipLayerArgs { + mod_hash: NFT_OWNERSHIP_LAYER_PUZZLE_HASH.into(), + current_owner, + transfer_program, + inner_puzzle: inner_spend.puzzle(), + }, + })?; + + let solution = ctx.alloc(NftOwnershipLayerSolution { + inner_solution: inner_spend.solution(), + })?; + + Ok(InnerSpend::new(puzzle, solution)) +} + +#[cfg(test)] +mod tests { + use super::*; + + use chia_bls::{sign, Signature}; + use chia_protocol::SpendBundle; + use chia_puzzles::{ + standard::{StandardArgs, STANDARD_PUZZLE_HASH}, + DeriveSynthetic, + }; + use clvm_utils::ToTreeHash; + use clvmr::Allocator; + + use crate::{ + testing::SECRET_KEY, CreateDid, IntermediateLauncher, Launcher, MintNft, RequiredSignature, + StandardDidSpend, StandardMint, WalletSimulator, + }; + + #[tokio::test] + async fn test_nft_lineage() -> anyhow::Result<()> { + let sim = WalletSimulator::new().await; + let peer = sim.peer().await; + + let mut allocator = Allocator::new(); + let mut ctx = SpendContext::new(&mut allocator); + + let sk = SECRET_KEY.derive_synthetic(); + let pk = sk.public_key(); + + let puzzle_hash = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { synthetic_key: pk }, + } + .tree_hash() + .into(); + + let parent = sim.generate_coin(puzzle_hash, 2).await.coin; + + let (create_did, did_info) = Launcher::new(parent.coin_id(), 1) + .create(&mut ctx)? + .create_standard_did(&mut ctx, pk)?; + + StandardSpend::new() + .chain(create_did) + .finish(&mut ctx, parent, pk)?; + + let (mint_nft, mut nft_info) = IntermediateLauncher::new(did_info.coin.coin_id(), 0, 1) + .create(&mut ctx)? + .mint_standard_nft( + &mut ctx, + StandardMint { + metadata: (), + royalty_puzzle_hash: puzzle_hash, + royalty_percentage: 300, + synthetic_key: pk, + owner_puzzle_hash: puzzle_hash, + did_id: did_info.launcher_id, + did_inner_puzzle_hash: did_info.did_inner_puzzle_hash, + }, + )?; + + let mut did_info = StandardDidSpend::new() + .chain(mint_nft) + .recreate() + .finish(&mut ctx, pk, did_info)?; + + for i in 0..5 { + let mut spend = StandardNftSpend::new().update(); + + if i % 2 == 0 { + spend = spend.new_owner(did_info.launcher_id, did_info.did_inner_puzzle_hash); + } + + let (nft_spend, new_nft_info) = spend.finish(&mut ctx, pk, nft_info)?; + nft_info = new_nft_info; + + did_info = StandardDidSpend::new() + .chain(nft_spend) + .recreate() + .finish(&mut ctx, pk, did_info)?; + } + + let coin_spends = ctx.take_spends(); + + let required_signatures = RequiredSignature::from_coin_spends( + &mut allocator, + &coin_spends, + WalletSimulator::AGG_SIG_ME.into(), + )?; + + let mut aggregated_signature = Signature::default(); + + for required in required_signatures { + aggregated_signature += &sign(&sk, required.final_message()); + } + + let ack = peer + .send_transaction(SpendBundle::new(coin_spends, aggregated_signature)) + .await?; + assert_eq!(ack.error, None); + assert_eq!(ack.status, 1); + + let coin_state = peer + .register_for_coin_updates(vec![did_info.coin.coin_id()], 0) + .await? + .remove(0); + assert_eq!(coin_state.coin, did_info.coin); + + let coin_state = peer + .register_for_coin_updates(vec![nft_info.coin.coin_id()], 0) + .await? + .remove(0); + assert_eq!(coin_state.coin, nft_info.coin); + + Ok(()) + } +} diff --git a/src/spends/puzzles/offer.rs b/src/spends/puzzles/offer.rs new file mode 100644 index 00000000..c596eb31 --- /dev/null +++ b/src/spends/puzzles/offer.rs @@ -0,0 +1,295 @@ +mod offer_builder; +mod offer_compression; +mod offer_encoding; +mod settlement_payments; + +pub use offer_builder::*; +pub use offer_compression::*; +pub use offer_encoding::*; +pub use settlement_payments::*; + +use chia_protocol::{CoinSpend, SpendBundle}; + +#[derive(Debug, Clone)] +pub struct Offer { + offered_spend_bundle: SpendBundle, + requested_payment_spends: Vec, +} + +impl From for Offer { + fn from(spend_bundle: SpendBundle) -> Self { + let (requested_payment_spends, coin_spends): (_, Vec<_>) = spend_bundle + .coin_spends + .into_iter() + .partition(|coin_spend| { + coin_spend + .coin + .parent_coin_info + .iter() + .all(|byte| *byte == 0) + }); + + let offered_spend_bundle = SpendBundle::new(coin_spends, spend_bundle.aggregated_signature); + + Self { + offered_spend_bundle, + requested_payment_spends, + } + } +} + +impl From for SpendBundle { + fn from(offer: Offer) -> Self { + let mut spend_bundle = offer.offered_spend_bundle; + + spend_bundle + .coin_spends + .extend(offer.requested_payment_spends); + + spend_bundle + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use chia_bls::{sign, DerivableKey, SecretKey, Signature}; + use chia_protocol::{Coin, SpendBundle}; + use chia_puzzles::{ + cat::{CatArgs, CAT_PUZZLE_HASH}, + offer::SETTLEMENT_PAYMENTS_PUZZLE_HASH, + standard::{StandardArgs, STANDARD_PUZZLE_HASH}, + DeriveSynthetic, LineageProof, + }; + use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; + use clvmr::Allocator; + + use crate::{ + testing::SECRET_KEY, AssertPuzzleAnnouncement, CatSpend, Chainable, CreateCoinWithMemos, + CreateCoinWithoutMemos, InnerSpend, IssueCat, RequiredSignature, SpendContext, + StandardSpend, WalletSimulator, + }; + + fn sk1() -> SecretKey { + SECRET_KEY.derive_unhardened(0).derive_synthetic() + } + + fn sk2() -> SecretKey { + SECRET_KEY.derive_unhardened(1).derive_synthetic() + } + + fn sign_tx(required_signatures: Vec) -> Signature { + let sk1 = sk1(); + let sk2 = sk2(); + + let pk1 = sk1.public_key(); + let pk2 = sk2.public_key(); + + let mut aggregated_signature = Signature::default(); + + for req in required_signatures { + if req.public_key() == pk1 { + let sig = sign(&sk1, &req.final_message()); + aggregated_signature += &sig; + } else if req.public_key() == pk2 { + let sig = sign(&sk2, &req.final_message()); + aggregated_signature += &sig; + } else { + panic!("unexpected public key"); + } + } + + aggregated_signature + } + + #[tokio::test] + async fn test_offer_bundle() -> anyhow::Result<()> { + let sim = WalletSimulator::new().await; + let peer = sim.peer().await; + + let mut allocator = Allocator::new(); + let mut ctx = SpendContext::new(&mut allocator); + + let sk = sk1(); + let pk = sk.public_key(); + + let puzzle_hash = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { synthetic_key: pk }, + } + .tree_hash() + .into(); + + let parent = sim.generate_coin(puzzle_hash, 1000).await.coin; + + let (issue_cat, cat_info) = IssueCat::new(parent.coin_id()) + .condition(ctx.alloc(CreateCoinWithMemos { + puzzle_hash, + amount: 1000, + memos: vec![puzzle_hash.to_vec().into()], + })?) + .multi_issuance(&mut ctx, pk, 1000)?; + + StandardSpend::new() + .chain(issue_cat) + .finish(&mut ctx, parent, pk)?; + + let coin_spends = ctx.take_spends(); + + let mut spend_bundle = SpendBundle::new(coin_spends, Signature::default()); + + let required_signatures = RequiredSignature::from_coin_spends( + ctx.allocator_mut(), + &spend_bundle.coin_spends, + WalletSimulator::AGG_SIG_ME.into(), + )?; + + spend_bundle.aggregated_signature = sign_tx(required_signatures); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.error, None); + assert_eq!(ack.status, 1); + + // Prepare offer contents. + let cat_puzzle_hash = CurriedProgram { + program: CAT_PUZZLE_HASH, + args: CatArgs { + mod_hash: CAT_PUZZLE_HASH.into(), + tail_program_hash: cat_info.asset_id, + inner_puzzle: TreeHash::from(puzzle_hash), + }, + } + .tree_hash() + .into(); + + let cat = Coin::new(cat_info.eve_coin.coin_id(), cat_puzzle_hash, 1000); + + let xch = sim.generate_coin(puzzle_hash, 1000).await.coin; + + let xch_payment = NotarizedPayment { + nonce: calculate_nonce(vec![cat.coin_id()]), + payments: vec![Payment::WithoutMemos(PaymentWithoutMemos { + puzzle_hash, + amount: 1000, + })], + }; + + let cat_payment = NotarizedPayment { + nonce: calculate_nonce(vec![xch.coin_id()]), + payments: vec![Payment::WithoutMemos(PaymentWithoutMemos { + puzzle_hash, + amount: 1000, + })], + }; + + let cat_puzzle = ctx.cat_puzzle(); + let settlement_payments_puzzle = ctx.settlement_payments_puzzle(); + + let cat_settlements = ctx.alloc(CurriedProgram { + program: cat_puzzle, + args: CatArgs { + mod_hash: CAT_PUZZLE_HASH.into(), + tail_program_hash: cat_info.asset_id, + inner_puzzle: settlement_payments_puzzle, + }, + })?; + + let cat_settlements_hash = ctx.tree_hash(cat_settlements); + + let assert_xch = offer_announcement_id( + &mut ctx, + SETTLEMENT_PAYMENTS_PUZZLE_HASH.into(), + xch_payment.clone(), + )?; + + let assert_cat = + offer_announcement_id(&mut ctx, cat_settlements_hash.into(), cat_payment.clone())?; + + let lineage_proof = LineageProof { + parent_parent_coin_id: parent.coin_id(), + parent_inner_puzzle_hash: cat_info.eve_inner_puzzle_hash, + parent_amount: 1000, + }; + + let inner_spend = StandardSpend::new() + .condition(ctx.alloc(CreateCoinWithMemos { + puzzle_hash: SETTLEMENT_PAYMENTS_PUZZLE_HASH.into(), + amount: 1000, + memos: vec![SETTLEMENT_PAYMENTS_PUZZLE_HASH.to_vec().into()], + })?) + .condition(ctx.alloc(AssertPuzzleAnnouncement { + announcement_id: assert_xch, + })?) + .inner_spend(&mut ctx, pk)?; + + CatSpend::new(cat_info.asset_id) + .spend(cat, inner_spend, lineage_proof, 0) + .finish(&mut ctx)?; + + let cat_puzzle_hash = CurriedProgram { + program: CAT_PUZZLE_HASH, + args: CatArgs { + mod_hash: CAT_PUZZLE_HASH.into(), + tail_program_hash: cat_info.asset_id, + inner_puzzle: SETTLEMENT_PAYMENTS_PUZZLE_HASH, + }, + } + .tree_hash() + .into(); + + let cat_settlement_coin = Coin::new(cat.coin_id(), cat_puzzle_hash, 1000); + + StandardSpend::new() + .condition(ctx.alloc(CreateCoinWithoutMemos { + puzzle_hash: SETTLEMENT_PAYMENTS_PUZZLE_HASH.into(), + amount: 1000, + })?) + .condition(ctx.alloc(AssertPuzzleAnnouncement { + announcement_id: assert_cat, + })?) + .finish(&mut ctx, xch, pk)?; + + let xch_settlement_coin = + Coin::new(xch.coin_id(), SETTLEMENT_PAYMENTS_PUZZLE_HASH.into(), 1000); + + let lineage_proof = LineageProof { + parent_parent_coin_id: cat_info.eve_coin.coin_id(), + parent_inner_puzzle_hash: puzzle_hash, + parent_amount: 1000, + }; + + let solution = ctx.alloc(SettlementPaymentsSolution { + notarized_payments: vec![cat_payment], + })?; + let inner_spend = InnerSpend::new(settlement_payments_puzzle, solution); + + CatSpend::new(cat_info.asset_id) + .spend(cat_settlement_coin, inner_spend, lineage_proof, 0) + .finish(&mut ctx)?; + + let puzzle_reveal = ctx.serialize(settlement_payments_puzzle)?; + let solution = ctx.serialize(SettlementPaymentsSolution { + notarized_payments: vec![xch_payment], + })?; + + ctx.spend(CoinSpend::new(xch_settlement_coin, puzzle_reveal, solution)); + + let coin_spends = ctx.take_spends(); + let mut spend_bundle = SpendBundle::new(coin_spends, Signature::default()); + + let required_signatures = RequiredSignature::from_coin_spends( + ctx.allocator_mut(), + &spend_bundle.coin_spends, + WalletSimulator::AGG_SIG_ME.into(), + )?; + + spend_bundle.aggregated_signature = sign_tx(required_signatures); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.error, None); + assert_eq!(ack.status, 1); + + Ok(()) + } +} diff --git a/src/spends/puzzles/offer/offer_builder.rs b/src/spends/puzzles/offer/offer_builder.rs new file mode 100644 index 00000000..8dce57cf --- /dev/null +++ b/src/spends/puzzles/offer/offer_builder.rs @@ -0,0 +1,165 @@ +use chia_protocol::{Bytes32, Coin, CoinSpend}; +use chia_puzzles::{ + cat::{CatArgs, CAT_PUZZLE_HASH}, + offer::SETTLEMENT_PAYMENTS_PUZZLE_HASH, +}; +use clvm_utils::{tree_hash_atom, tree_hash_pair, CurriedProgram, ToTreeHash}; +use clvmr::NodePtr; +use sha2::{digest::FixedOutput, Digest, Sha256}; + +use crate::{ + AssertPuzzleAnnouncement, ChainedSpend, NotarizedPayment, Payment, SettlementPaymentsSolution, + SpendContext, SpendError, +}; + +pub struct OfferBuilder { + nonce: Bytes32, + coin_spends: Vec, + parent_conditions: Vec, +} + +impl OfferBuilder { + pub fn new(offered_coin_ids: Vec) -> Self { + let nonce = calculate_nonce(offered_coin_ids); + Self::from_nonce(nonce) + } + + pub fn from_nonce(nonce: Bytes32) -> Self { + Self { + nonce, + coin_spends: Vec::new(), + parent_conditions: Vec::new(), + } + } + + pub fn request_xch_payments( + self, + ctx: &mut SpendContext, + payments: Vec, + ) -> Result { + let puzzle = ctx.standard_puzzle(); + self.request_payments(ctx, puzzle, payments) + } + + pub fn request_cat_payments( + self, + ctx: &mut SpendContext, + asset_id: Bytes32, + payments: Vec, + ) -> Result { + let puzzle_hash = CurriedProgram { + program: CAT_PUZZLE_HASH, + args: CatArgs { + mod_hash: CAT_PUZZLE_HASH.into(), + tail_program_hash: asset_id, + inner_puzzle: SETTLEMENT_PAYMENTS_PUZZLE_HASH, + }, + } + .tree_hash(); + + let puzzle = if let Some(puzzle) = ctx.get_puzzle(&puzzle_hash) { + puzzle + } else { + let cat_puzzle = ctx.cat_puzzle(); + let settlement_payments_puzzle = ctx.settlement_payments_puzzle(); + let puzzle = ctx.alloc(CurriedProgram { + program: cat_puzzle, + args: CatArgs { + mod_hash: CAT_PUZZLE_HASH.into(), + tail_program_hash: asset_id, + inner_puzzle: settlement_payments_puzzle, + }, + })?; + ctx.preload(puzzle_hash, puzzle); + puzzle + }; + + self.request_payments(ctx, puzzle, payments) + } + + pub fn request_payments( + mut self, + ctx: &mut SpendContext, + puzzle: NodePtr, + payments: Vec, + ) -> Result { + let (coin_spend, announcement_id) = + request_offer_payments(ctx, self.nonce, puzzle, payments)?; + + self.coin_spends.push(coin_spend); + self.parent_conditions + .push(ctx.alloc(AssertPuzzleAnnouncement { announcement_id })?); + + Ok(self) + } + + pub fn finish(self, ctx: &mut SpendContext) -> ChainedSpend { + for coin_spend in self.coin_spends { + ctx.spend(coin_spend); + } + + ChainedSpend { + parent_conditions: self.parent_conditions, + } + } +} + +pub fn calculate_nonce(offered_coin_ids: Vec) -> Bytes32 { + let mut coin_ids = offered_coin_ids; + coin_ids.sort(); + + let mut tree_hash = tree_hash_atom(&[]); + + for coin_id in coin_ids.into_iter().rev() { + let item_hash = tree_hash_atom(&coin_id); + tree_hash = tree_hash_pair(item_hash, tree_hash); + } + + tree_hash.into() +} + +pub fn request_offer_payments( + ctx: &mut SpendContext, + nonce: Bytes32, + puzzle: NodePtr, + payments: Vec, +) -> Result<(CoinSpend, Bytes32), SpendError> { + let puzzle_reveal = ctx.serialize(puzzle)?; + let puzzle_hash = ctx.tree_hash(puzzle).into(); + + let notarized_payment = NotarizedPayment { nonce, payments }; + + let settlement_solution = ctx.serialize(SettlementPaymentsSolution { + notarized_payments: vec![notarized_payment.clone()], + })?; + + let coin_spend = CoinSpend::new( + Coin::new(Bytes32::default(), puzzle_hash, 0), + puzzle_reveal, + settlement_solution, + ); + + let notarized_payment_ptr = ctx.alloc(notarized_payment)?; + let notarized_payment_hash = ctx.tree_hash(notarized_payment_ptr); + + let mut hasher = Sha256::new(); + hasher.update(puzzle_hash); + hasher.update(notarized_payment_hash); + let puzzle_announcement_id = Bytes32::new(hasher.finalize_fixed().into()); + + Ok((coin_spend, puzzle_announcement_id)) +} + +pub fn offer_announcement_id( + ctx: &mut SpendContext, + puzzle_hash: Bytes32, + notarized_payment: NotarizedPayment, +) -> Result { + let notarized_payment = ctx.alloc(notarized_payment)?; + let notarized_payment_hash = ctx.tree_hash(notarized_payment); + + let mut hasher = Sha256::new(); + hasher.update(puzzle_hash); + hasher.update(notarized_payment_hash); + Ok(Bytes32::new(hasher.finalize_fixed().into())) +} diff --git a/src/spends/puzzles/offer/offer_compression.rs b/src/spends/puzzles/offer/offer_compression.rs new file mode 100644 index 00000000..12461e96 --- /dev/null +++ b/src/spends/puzzles/offer/offer_compression.rs @@ -0,0 +1,215 @@ +use std::{ + array::TryFromSliceError, + io::{self, ErrorKind, Read}, +}; + +use chia_protocol::SpendBundle; +use chia_traits::Streamable; +use chia_puzzles::{ + cat::{CAT_PUZZLE, CAT_PUZZLE_V1}, + nft::{ + NFT_METADATA_UPDATER_PUZZLE, NFT_OWNERSHIP_LAYER_PUZZLE, NFT_ROYALTY_TRANSFER_PUZZLE, + NFT_STATE_LAYER_PUZZLE, + }, + offer::{SETTLEMENT_PAYMENTS_PUZZLE, SETTLEMENT_PAYMENTS_PUZZLE_V1}, + singleton::SINGLETON_TOP_LAYER_PUZZLE, + standard::STANDARD_PUZZLE, +}; +use flate2::{ + read::{ZlibDecoder, ZlibEncoder}, + Compress, Compression, Decompress, FlushDecompress, +}; +use thiserror::Error; + +macro_rules! define_compression_versions { + ( $( $version:expr => $( $bytes:expr ),+ ; )+ ) => { + fn zdict_for_version(version: u16) -> Vec { + let mut bytes = Vec::new(); + $( if version >= $version { + $( bytes.extend_from_slice(&$bytes); )+ + } )+ + bytes + } + + /// Returns the required compression version for the given puzzle reveals. + pub fn required_compression_version(puzzles: Vec>) -> u16 { + let mut required_version = MIN_VERSION; + $( { + $( if required_version < $version && puzzles.iter().any(|puzzle| puzzle == &$bytes) { + required_version = $version; + } )+ + } )+ + required_version + } + }; +} + +const MIN_VERSION: u16 = 6; +const MAX_VERSION: u16 = 6; + +define_compression_versions!( + 1 => STANDARD_PUZZLE, CAT_PUZZLE_V1; + 2 => SETTLEMENT_PAYMENTS_PUZZLE_V1; + 3 => SINGLETON_TOP_LAYER_PUZZLE, NFT_STATE_LAYER_PUZZLE, + NFT_OWNERSHIP_LAYER_PUZZLE, NFT_METADATA_UPDATER_PUZZLE, + NFT_ROYALTY_TRANSFER_PUZZLE; + 4 => CAT_PUZZLE; + 5 => SETTLEMENT_PAYMENTS_PUZZLE; + 6 => [0; 0]; // Purposefully break backwards compatibility. +); + +/// An error than can occur while decompressing an offer. +#[derive(Debug, Error)] +pub enum DecompressionError { + /// An io error. + #[error("io error: {0}")] + Io(#[from] io::Error), + + /// An error that occurred while trying to convert a slice to an array. + #[error("{0}")] + TryFromSlice(#[from] TryFromSliceError), + + /// The input is missing the version prefix. + #[error("missing version prefix")] + MissingVersionPrefix, + + /// The version is unsupported. + #[error("unsupported version")] + UnsupportedVersion, + + /// A streamable error. + #[error("streamable error: {0}")] + Streamable(#[from] chia_traits::Error), +} + +/// Decompresses an offer spend bundle. +pub fn decompress_offer(bytes: &[u8]) -> Result { + let decompressed_bytes = decompress_offer_bytes(bytes)?; + Ok(SpendBundle::from_bytes(&decompressed_bytes)?) +} + +/// Decompresses an offer spend bundle into bytes. +pub fn decompress_offer_bytes(bytes: &[u8]) -> Result, DecompressionError> { + let version_bytes: [u8; 2] = bytes + .get(0..2) + .ok_or(DecompressionError::MissingVersionPrefix)? + .try_into()?; + + let version = u16::from_be_bytes(version_bytes); + + if version > MAX_VERSION { + return Err(DecompressionError::UnsupportedVersion); + } + + let zdict = zdict_for_version(version); + + Ok(decompress(&bytes[2..], &zdict)?) +} + +#[derive(Debug, Error)] +pub enum CompressionError { + #[error("io error: {0}")] + Io(#[from] io::Error), + #[error("streamable error: {0}")] + Streamable(#[from] chia_traits::Error), +} + +/// Compresses an offer spend bundle. +pub fn compress_offer(spend_bundle: SpendBundle) -> Result, CompressionError> { + let bytes = spend_bundle.to_bytes()?; + let version = required_compression_version( + spend_bundle + .coin_spends + .into_iter() + .map(|cs| cs.puzzle_reveal.to_vec()) + .collect(), + ); + Ok(compress_offer_bytes(&bytes, version)?) +} + +/// Compresses an offer spend bundle from bytes. +pub fn compress_offer_bytes(bytes: &[u8], version: u16) -> io::Result> { + let mut output = version.to_be_bytes().to_vec(); + let zdict = zdict_for_version(version); + output.extend(compress(bytes, &zdict)?); + Ok(output) +} + +fn decompress(input: &[u8], zdict: &[u8]) -> io::Result> { + let mut decompress = Decompress::new(true); + if decompress + .decompress(input, &mut [], FlushDecompress::Finish) + .is_ok() + { + return Err(io::Error::new( + ErrorKind::Unsupported, + "cannot decompress uncompressed input", + )); + } + decompress.set_dictionary(zdict)?; + let i = decompress.total_in(); + let mut decoder = ZlibDecoder::new_with_decompress(&input[i as usize..], decompress); + let mut output = Vec::new(); + decoder.read_to_end(&mut output)?; + Ok(output) +} + +fn compress(input: &[u8], zdict: &[u8]) -> io::Result> { + let mut compress = Compress::new(Compression::new(6), true); + compress.set_dictionary(zdict)?; + let mut encoder = ZlibEncoder::new_with_compress(input, compress); + let mut output = Vec::new(); + encoder.read_to_end(&mut output)?; + Ok(output) +} + +#[cfg(test)] +mod tests { + use chia_protocol::SpendBundle; + use chia_traits::Streamable; + use hex::ToHex; + use hex_literal::hex; + + use super::*; + + #[test] + fn test_compression() { + for version in MIN_VERSION..=MAX_VERSION { + let output = compress_offer_bytes(&DECOMPRESSED_OFFER_HEX, version).unwrap(); + + assert_eq!( + output.encode_hex::(), + COMPRESSED_OFFER_HEX.encode_hex::() + ); + } + } + + #[test] + fn test_decompression() { + for _ in MIN_VERSION..=MAX_VERSION { + let output = decompress_offer_bytes(&COMPRESSED_OFFER_HEX).unwrap(); + + assert_eq!( + output.encode_hex::(), + DECOMPRESSED_OFFER_HEX.encode_hex::() + ); + } + } + + #[test] + fn parse_spend_bundle() { + SpendBundle::from_bytes(&DECOMPRESSED_OFFER_HEX).unwrap(); + } + + const COMPRESSED_OFFER_HEX: [u8; 1225] = hex!( + " + 000678bb1ce2864b63606060622000f234ef6ae4725635979d975e6263fb68c9b687879b720f54fbcf0ace3de319c33d09a60ede4e1dadfd476bffd1da7fb4f61fadfd476bffff0355fb830bf705e6fb3e27bc6b6d34de3637326a42ee9158c6f501e673d706c6ed0cecda5d23ab550555c6f445a3f9b7b1852187eac9c0f7675608bcd9265e74f3e9ad2c5d5faed4893b3aeba055c5688b8288ee3f234466c1f5c59bc3dfd96dbcdc5aa327e6fd6ee9e99e8f172eb31fde71c2cdb66561eca3c7af804aa6d4443f0cd2fa1f6eb6577ba710cb9fc5ad697cf64be7596f5ffcae607e50c3fcc2ffade21e652f18885009b273febfbd99c12d7de60d5c57358d8e1dbbc795a9b0a6d4e9ea910ef9d007a50deed925613fb414ea5ca30f2eaff9a0f4cff964c7a693676d2fdd7448ba1514b7f1505130b419038f6678188fae18185d31008ef9d1150330c121b3620092b617d4af320ade7ff7e2837ba739d936dd4972df7de47c427f8449acb8d5ede4175b6ae6ff5ff0728acd3aa766d7c7f97cbeba7667cf17fb3d3bd6da671653ffe68cd6bc97befbad17bcff50aae419907020ed5bbed19a43cbebacec8aab4e75af5b727199df92b3076d57a2d611ff47d729d02e018cae5318beeb14c0097ec102dff9e165fe214bec1c0cf6fdf66d60ca6b67955cb52cd964dd0f03ff58e6c98760f9a2f4ff81c88c929282622b7dfda4c4b4eca2d4cca48ca28ad2a2ca34a3f4f2caf4e28ad4ec9c8cc4f492dc948a92e2fce4c2b2acacaa94c24a3393a2ac9432e3a294bc94c2bc7413bdcc82b462bdbcb492e292fca2c4f454bd9cccbc6cd0b044c602f5de9732e26edb78b5277a5bf09af17d9edccab5d452c6f09dabf2ba47667dbce6ffff37e596fe5f087344416271496a52669e5e727eae7e5162b97e88a98f7b8153aa71034861c682c008a6f8681599bc832d9d45963116a61e53e2d30e2672ff3a706f25fbb6b8de04a0aae23cd0fc567109ac665ef0afcbdb4f7dd1a359bef6c7cfcd0daaba7deaccae6d6b174fb0396fbd7279e6a5ebdd584a9ed1651e28096c7499c7e8320f02eef93fbacc83d878a2cf320f48d97774f5abcaf5ab18a443d4ef2f3871dee954bec5d716e98a9f9b0f0515db6dbaa7ae0655f6b0ab32d0e47262d7295619afd882af4b7c76f1e9aee7c830e0e672d73b14f53d0f4b1139ba9c627439c550584ef19fce3d98058b9d16dabff3fbb2ebedec9fe9db2f4f375cbab5f77ef5c4d939f939dfee2e5e3af10c4419176ca0075bc61ab6db1bc1066fe87931a1f4d68ffcc0c247870433ef3c8b78eae9b54e6642cd87e78eb9c71faadd158fdb9f67bae7e5b740d35b9d53ea1e8a9e428c8ba150ff09c7d9ff0561ed81a593f6ef60faee68c15eebbe42397dcad515b981b24d816a1e7a96d3e7f2805d061abc013b18d87803316003670b08cddd413202b01bb4e0fcfe7b6f637c16ddb3e5feaab0f3b4f7f6122bd3d9979e2decb838e7f9fd3f8a979f830c27a8086496fdff0552f1cc53263ead78fe2ffafe9edf3b5e6e49a98cf874ffa813df0ae3b5f6bf2a5bf361f9b569fdd32dedbf5a8e9cbbe35da7cd7dfda2a038fff584f2522b734be70949afef545888cc3df25eee72421443edffd2dfd3bef2a9b15e5e3ed3e34ca2e57efe075e923e3badde58d8258b35873ebebb69fd156571cbe7f20fa6a4fbea7c0f5772da3e554d190018bbefd7 + " + ); + + const DECOMPRESSED_OFFER_HEX: [u8; 7390] = hex!( + " + 0000000200000000000000000000000000000000000000000000000000000000000000006e29dd286d097a8376cf1ba43c3de2a4b6e1c3826dc07b4f9a536dcc495c0b920000000000000000ff02ffff01ff02ffff01ff02ff5effff04ff02ffff04ffff04ff05ffff04ffff0bff34ff0580ffff04ff0bff80808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ffff02ff2effff04ff02ffff04ff17ff80808080ffff04ffff02ff2affff04ff02ffff04ff82027fffff04ff82057fffff04ff820b7fff808080808080ffff04ff81bfffff04ff82017fffff04ff8202ffffff04ff8205ffffff04ff820bffff80808080808080808080808080ffff04ffff01ffffffff3d46ff02ff333cffff0401ff01ff81cb02ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff7cffff0bff34ff2480ffff0bff7cffff0bff7cffff0bff34ff2c80ff0980ffff0bff7cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ffff02ff2effff04ff02ffff04ff13ff80808080ff820b9f80ffff01ff02ff56ffff04ff02ffff04ffff02ff13ffff04ff5fffff04ff17ffff04ff2fffff04ff81bfffff04ff82017fffff04ff1bff8080808080808080ffff04ff82017fff8080808080ffff01ff088080ff0180ffff01ff02ffff03ff17ffff01ff02ffff03ffff20ff81bf80ffff0182017fffff01ff088080ff0180ffff01ff088080ff018080ff0180ff04ffff04ff05ff2780ffff04ffff10ff0bff5780ff778080ffffff02ffff03ff05ffff01ff02ffff03ffff09ffff02ffff03ffff09ff11ff5880ffff0159ff8080ff0180ffff01818f80ffff01ff02ff26ffff04ff02ffff04ff0dffff04ff0bffff04ffff04ff81b9ff82017980ff808080808080ffff01ff02ff7affff04ff02ffff04ffff02ffff03ffff09ff11ff5880ffff01ff04ff58ffff04ffff02ff76ffff04ff02ffff04ff13ffff04ff29ffff04ffff0bff34ff5b80ffff04ff2bff80808080808080ff398080ffff01ff02ffff03ffff09ff11ff7880ffff01ff02ffff03ffff20ffff02ffff03ffff09ffff0121ffff0dff298080ffff01ff02ffff03ffff09ffff0cff29ff80ff3480ff5c80ffff01ff0101ff8080ff0180ff8080ff018080ffff0109ffff01ff088080ff0180ffff010980ff018080ff0180ffff04ffff02ffff03ffff09ff11ff5880ffff0159ff8080ff0180ffff04ffff02ff26ffff04ff02ffff04ff0dffff04ff0bffff04ff17ff808080808080ff80808080808080ff0180ffff01ff04ff80ffff04ff80ff17808080ff0180ffff02ffff03ff05ffff01ff04ff09ffff02ff56ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff0bff7cffff0bff34ff2880ffff0bff7cffff0bff7cffff0bff34ff2c80ff0580ffff0bff7cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff04ffff04ff30ffff04ff5fff808080ffff02ff7effff04ff02ffff04ffff04ffff04ff2fff0580ffff04ff5fff82017f8080ffff04ffff02ff26ffff04ff02ffff04ff0bffff04ff05ffff01ff808080808080ffff04ff17ffff04ff81bfffff04ff82017fffff04ffff02ff2affff04ff02ffff04ff8204ffffff04ffff02ff76ffff04ff02ffff04ff09ffff04ff820affffff04ffff0bff34ff2d80ffff04ff15ff80808080808080ffff04ff8216ffff808080808080ffff04ff8205ffffff04ff820bffff808080808080808080808080ff02ff5affff04ff02ffff04ff5fffff04ff3bffff04ffff02ffff03ff17ffff01ff09ff2dffff02ff2affff04ff02ffff04ff27ffff04ffff02ff76ffff04ff02ffff04ff29ffff04ff57ffff04ffff0bff34ff81b980ffff04ff59ff80808080808080ffff04ff81b7ff80808080808080ff8080ff0180ffff04ff17ffff04ff05ffff04ff8202ffffff04ffff04ffff04ff78ffff04ffff0eff5cffff02ff2effff04ff02ffff04ffff04ff2fffff04ff82017fff808080ff8080808080ff808080ffff04ffff04ff20ffff04ffff0bff81bfff5cffff02ff2effff04ff02ffff04ffff04ff15ffff04ffff10ff82017fffff11ff8202dfff2b80ff8202ff80ff808080ff8080808080ff808080ff138080ff80808080808080808080ff018080ffff04ffff01a037bef360ee858133b69d595a906dc45d01af50379dad515eb9518abb7c1d2a7affff04ffff01a002f42883fb3338310825c951efcca810ecb61772d9e5da6a2d4d0a6591b8897effff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff0180808080ffffa0d7a3b357ee3eb1d3857c2e164beea5cb8cf1d0d307c3b8c8463d84a15de2e3eaffffa0947c5be1522aff5736bd2bb91204fca385660e3fa59e3bb7a3ee709f52809f71ff85174876e800ffffa0947c5be1522aff5736bd2bb91204fca385660e3fa59e3bb7a3ee709f52809f71808080809ffebd6953848e37800ad52932c6c6de0a6920ac7542d5c4881f55e07580476b7456f82a207e455bc1a77cf022fe43c988b2c9cd3dd2d94062da525eb1c272530000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0e9943cae428345e36f0e4d2d3ecdcf734ee6c6858e365c7feccc2a9ee94dbf3ba0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffff75ffc05968747470733a2f2f6261666b726569626872787572796632677779677378656b6c686167746d647874736f6371766a6a7a6471793634726a64763372646e64716e67342e697066732e6e667473746f726167652e6c696e6b2f80ffff68a0278de91c1746b60d2b914b380d360ef393850aa5391c31ee4523aee2368e0d37ffff826d75ffa168747470733a2f2f706173746562696e2e636f6d2f7261772f54354c477042653380ffff826d68a05158025f5b241c6ec1848972395c383548945f66c1610bfac0dea907b65e8d60ffff82736e01ffff8273740180ffff04ffff01a0fe8a4b4e27a2e29a4d3fc7ce9d527adbcaccbab6ada3903ccf3ba9a769d2d78bffff04ffff01ff02ffff01ff02ffff01ff02ff26ffff04ff02ffff04ff05ffff04ff17ffff04ff0bffff04ffff02ff2fff5f80ff80808080808080ffff04ffff01ffffff82ad4cff0233ffff3e04ff81f601ffffff0102ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff32ffff0bff3cff3480ffff0bff32ffff0bff32ffff0bff3cff2280ff0980ffff0bff32ff0bffff0bff3cff8080808080ff8080808080ffff010b80ff0180ff04ffff04ff38ffff04ffff02ff36ffff04ff02ffff04ff05ffff04ff27ffff04ffff02ff2effff04ff02ffff04ffff02ffff03ff81afffff0181afffff010b80ff0180ff80808080ffff04ffff0bff3cff4f80ffff04ffff0bff3cff0580ff8080808080808080ff378080ff82016f80ffffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff2fffff01ff80ff808080808080808080ff0bff32ffff0bff3cff2880ffff0bff32ffff0bff32ffff0bff3cff2280ff0580ffff0bff32ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff5fffff01ff02ffff03ffff09ff82011fff3880ffff01ff02ffff03ffff09ffff18ff82059f80ff3c80ffff01ff02ffff03ffff20ff81bf80ffff01ff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff82019fffff04ff82017fff80808080808080808080ffff01ff088080ff0180ffff01ff04ff819fffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ff82017fff808080808080808080808080ff0180ffff01ff02ffff03ffff09ff82011fff2c80ffff01ff02ffff03ffff20ff82017f80ffff01ff04ffff04ff24ffff04ffff0eff10ffff02ff2effff04ff02ffff04ff82019fff8080808080ff808080ffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ffff02ff0bffff04ff17ffff04ff2fffff04ff82019fff8080808080ff8080808080808080808080ffff01ff088080ff0180ffff01ff02ffff03ffff09ff82011fff2480ffff01ff02ffff03ffff20ffff02ffff03ffff09ffff0122ffff0dff82029f8080ffff01ff02ffff03ffff09ffff0cff82029fff80ffff010280ff1080ffff01ff0101ff8080ff0180ff8080ff018080ffff01ff04ff819fffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ff82017fff8080808080808080808080ffff01ff088080ff0180ffff01ff04ff819fffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ff82017fff808080808080808080808080ff018080ff018080ff0180ffff01ff02ff3affff04ff02ffff04ff05ffff04ff0bffff04ff81bfffff04ffff02ffff03ff82017fffff0182017fffff01ff02ff0bffff04ff17ffff04ff2fffff01ff808080808080ff0180ff8080808080808080ff0180ff018080ffff04ffff01a0c5abea79afaa001b5427dfa0c8cf42ca6f38f5841b78f9b3c252733eb2de2726ffff04ffff01a0e18a795134d3618aca051c4a5d70f5a44cba0e2daf0868300b0a472ec25af76effff04ffff01ff02ffff01ff02ffff01ff02ffff03ff81bfffff01ff04ff82013fffff04ff80ffff04ffff02ffff03ffff22ff82013fffff20ffff09ff82013fff2f808080ffff01ff04ffff04ff10ffff04ffff0bffff02ff2effff04ff02ffff04ff09ffff04ff8205bfffff04ffff02ff3effff04ff02ffff04ffff04ff09ffff04ff82013fff1d8080ff80808080ff808080808080ff1580ff808080ffff02ff16ffff04ff02ffff04ff0bffff04ff17ffff04ff8202bfffff04ff15ff8080808080808080ffff01ff02ff16ffff04ff02ffff04ff0bffff04ff17ffff04ff8202bfffff04ff15ff8080808080808080ff0180ff80808080ffff01ff04ff2fffff01ff80ff80808080ff0180ffff04ffff01ffffff3f02ff04ff0101ffff822710ff02ff02ffff03ff05ffff01ff02ff3affff04ff02ffff04ff0dffff04ffff0bff2affff0bff2cff1480ffff0bff2affff0bff2affff0bff2cff3c80ff0980ffff0bff2aff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff17ffff01ff04ffff04ff10ffff04ffff0bff81a7ffff02ff3effff04ff02ffff04ffff04ff2fffff04ffff04ff05ffff04ffff05ffff14ffff12ff47ff0b80ff128080ffff04ffff04ff05ff8080ff80808080ff808080ff8080808080ff808080ffff02ff16ffff04ff02ffff04ff05ffff04ff0bffff04ff37ffff04ff2fff8080808080808080ff8080ff0180ffff0bff2affff0bff2cff1880ffff0bff2affff0bff2affff0bff2cff3c80ff0580ffff0bff2affff02ff3affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0e9943cae428345e36f0e4d2d3ecdcf734ee6c6858e365c7feccc2a9ee94dbf3ba0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01a0a342a13fee4ef4baed9bf967b7d39731a5b58ddf7b919b6c6f6cf6dda3a591ccffff04ffff010aff0180808080ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b08ce89075daf86f5171e2c21169dce658e5494aae1c907cf0e7416dc7e126dd175ebf6e35bce9f65135da89947ee115caff018080ff018080808080ff018080808080ff01808080ffffa0e9943cae428345e36f0e4d2d3ecdcf734ee6c6858e365c7feccc2a9ee94dbf3bffa05687517592bfb802f74138077d47a8236794d5a86d511d825126482e39979d0cff0180ff01ffffffff80ffff01ffff81f6ff80ffffff85174876e800ffa06e29dd286d097a8376cf1ba43c3de2a4b6e1c3826dc07b4f9a536dcc495c0b928080ff8080ffff33ffa0cfbfdeed5c4ca2de3d0bf520b9cb4bb7743a359bd2e6a188d19ce7dffc21d3e7ff01ffffa0cfbfdeed5c4ca2de3d0bf520b9cb4bb7743a359bd2e6a188d19ce7dffc21d3e78080ffff3fffa01a5f039491e578e7fe5bdfbcfbb8e9b4647958f2dfc5420ea833ad3ffa79856f8080ff808080808082afe5b487fa84c4cedc4b7e2b0bd7d111170fd76077753a3739439062ebdc7838149dc4ef1ed3605a007dff75fb96f50e2605d3a79948cc6139bf0fe04a194cb93aec383e63168355e3ddb2afd4231739e71fe094674d2cf7572242b7952623 + " + ); +} diff --git a/src/spends/puzzles/offer/offer_encoding.rs b/src/spends/puzzles/offer/offer_encoding.rs new file mode 100644 index 00000000..1ed00e5c --- /dev/null +++ b/src/spends/puzzles/offer/offer_encoding.rs @@ -0,0 +1,43 @@ +use bech32::{u5, Variant}; +use thiserror::Error; + +/// Errors you can get while trying to decode an offer. +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum DecodeOfferError { + /// The wrong HRP prefix was used. + #[error("invalid prefix `{0}`")] + InvalidPrefix(String), + + /// The address was encoded as bech32, rather than bech32m. + #[error("encoding is not bech32m")] + InvalidFormat, + + /// An error occured while trying to decode the address. + #[error("error when decoding address: {0}")] + Decode(#[from] bech32::Error), +} + +/// Decodes an offer into bytes. +pub fn decode_offer(offer: &str) -> Result, DecodeOfferError> { + let (hrp, data, variant) = bech32::decode(offer)?; + + if variant != Variant::Bech32m { + return Err(DecodeOfferError::InvalidFormat); + } + + if hrp.as_str() != "offer" { + return Err(DecodeOfferError::InvalidPrefix(hrp)); + } + + Ok(bech32::convert_bits(&data, 5, 8, false)?) +} + +/// Encodes an offer. +pub fn encode_offer(offer: &[u8]) -> Result { + let data = bech32::convert_bits(offer, 8, 5, true) + .unwrap() + .into_iter() + .map(u5::try_from_u8) + .collect::, bech32::Error>>()?; + bech32::encode("offer1", data, Variant::Bech32m) +} diff --git a/src/spends/puzzles/offer/settlement_payments.rs b/src/spends/puzzles/offer/settlement_payments.rs new file mode 100644 index 00000000..4b608c07 --- /dev/null +++ b/src/spends/puzzles/offer/settlement_payments.rs @@ -0,0 +1,37 @@ +use chia_protocol::{Bytes, Bytes32}; +use clvm_traits::{FromClvm, ToClvm}; + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(tuple)] +pub struct SettlementPaymentsSolution { + pub notarized_payments: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(tuple)] +pub struct NotarizedPayment { + pub nonce: Bytes32, + pub payments: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(tuple, untagged)] +pub enum Payment { + WithoutMemos(PaymentWithoutMemos), + WithMemos(PaymentWithMemos), +} + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct PaymentWithoutMemos { + pub puzzle_hash: Bytes32, + pub amount: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct PaymentWithMemos { + pub puzzle_hash: Bytes32, + pub amount: u64, + pub memos: Vec, +} diff --git a/src/spends/puzzles/singleton.rs b/src/spends/puzzles/singleton.rs new file mode 100644 index 00000000..0c6b4e1d --- /dev/null +++ b/src/spends/puzzles/singleton.rs @@ -0,0 +1,9 @@ +mod intermediate_launcher; +mod launcher; +mod singleton_spend; +mod spendable_launcher; + +pub use intermediate_launcher::*; +pub use launcher::*; +pub use singleton_spend::*; +pub use spendable_launcher::*; diff --git a/src/spends/puzzles/singleton/intermediate_launcher.rs b/src/spends/puzzles/singleton/intermediate_launcher.rs new file mode 100644 index 00000000..0ee5804c --- /dev/null +++ b/src/spends/puzzles/singleton/intermediate_launcher.rs @@ -0,0 +1,140 @@ +use chia_protocol::{Bytes32, Coin, CoinSpend}; +use chia_puzzles::{ + nft::{NftIntermediateLauncherArgs, NFT_INTERMEDIATE_LAUNCHER_PUZZLE_HASH}, + singleton::SINGLETON_LAUNCHER_PUZZLE_HASH, +}; +use clvm_utils::{curry_tree_hash, tree_hash_atom, CurriedProgram}; +use sha2::{Digest, Sha256}; + +use crate::{ + usize_to_bytes, AssertCoinAnnouncement, ChainedSpend, CreateCoinWithoutMemos, SpendContext, + SpendError, SpendableLauncher, +}; + +pub struct IntermediateLauncher { + mint_number: usize, + mint_total: usize, + intermediate_coin: Coin, + launcher_coin: Coin, +} + +impl IntermediateLauncher { + pub fn new(parent_coin_id: Bytes32, mint_number: usize, mint_total: usize) -> Self { + let puzzle_hash = intermediate_launcher_puzzle_hash(mint_number, mint_total); + let intermediate_coin = Coin::new(parent_coin_id, puzzle_hash, 0); + let launcher_coin = Coin::new( + intermediate_coin.coin_id(), + SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + 1, + ); + Self { + mint_number, + mint_total, + intermediate_coin, + launcher_coin, + } + } + + pub fn intermediate_coin(&self) -> Coin { + self.intermediate_coin + } + + pub fn launcher_coin(&self) -> Coin { + self.launcher_coin + } + + pub fn create(self, ctx: &mut SpendContext) -> Result { + let mut parent_conditions = Vec::new(); + + let intermediate_puzzle = ctx.nft_intermediate_launcher(); + + let puzzle = ctx.alloc(CurriedProgram { + program: intermediate_puzzle, + args: NftIntermediateLauncherArgs { + launcher_puzzle_hash: SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + mint_number: self.mint_number, + mint_total: self.mint_total, + }, + })?; + + let puzzle_hash = ctx.tree_hash(puzzle); + + parent_conditions.push(ctx.alloc(CreateCoinWithoutMemos { + puzzle_hash: puzzle_hash.into(), + amount: 0, + })?); + + let puzzle_reveal = ctx.serialize(puzzle)?; + let solution = ctx.serialize(())?; + + let intermediate_id = self.intermediate_coin.coin_id(); + + ctx.spend(CoinSpend::new( + self.intermediate_coin, + puzzle_reveal, + solution, + )); + + let mut index_message = Sha256::new(); + index_message.update(usize_to_bytes(self.mint_number)); + index_message.update(usize_to_bytes(self.mint_total)); + + let mut announcement_id = Sha256::new(); + announcement_id.update(intermediate_id); + announcement_id.update(index_message.finalize()); + + parent_conditions.push(ctx.alloc(AssertCoinAnnouncement { + announcement_id: Bytes32::new(announcement_id.finalize().into()), + })?); + + let chained_spend = ChainedSpend { parent_conditions }; + + Ok(SpendableLauncher::new(self.launcher_coin, chained_spend)) + } +} + +fn intermediate_launcher_puzzle_hash(mint_number: usize, mint_total: usize) -> Bytes32 { + let launcher_hash = tree_hash_atom(&SINGLETON_LAUNCHER_PUZZLE_HASH); + let mint_number_hash = tree_hash_atom(&usize_to_bytes(mint_number)); + let mint_total_hash = tree_hash_atom(&usize_to_bytes(mint_total)); + + curry_tree_hash( + NFT_INTERMEDIATE_LAUNCHER_PUZZLE_HASH, + &[launcher_hash, mint_number_hash, mint_total_hash], + ) + .into() +} + +#[cfg(test)] +mod tests { + use clvmr::Allocator; + + use super::*; + + #[test] + fn test_intermediate_hash() { + let mut allocator = Allocator::new(); + let mut ctx = SpendContext::new(&mut allocator); + + let mint_number = 3; + let mint_total = 4; + + let intermediate_launcher_puzzle = ctx.nft_intermediate_launcher(); + + let puzzle = ctx + .alloc(CurriedProgram { + program: intermediate_launcher_puzzle, + args: NftIntermediateLauncherArgs { + launcher_puzzle_hash: SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + mint_number, + mint_total, + }, + }) + .unwrap(); + let allocated_puzzle_hash = ctx.tree_hash(puzzle); + + let puzzle_hash = intermediate_launcher_puzzle_hash(mint_number, mint_total); + + assert_eq!(hex::encode(allocated_puzzle_hash), hex::encode(puzzle_hash)); + } +} diff --git a/src/spends/puzzles/singleton/launcher.rs b/src/spends/puzzles/singleton/launcher.rs new file mode 100644 index 00000000..99d604cd --- /dev/null +++ b/src/spends/puzzles/singleton/launcher.rs @@ -0,0 +1,105 @@ +use chia_protocol::{Bytes32, Coin}; +use chia_puzzles::singleton::{SINGLETON_LAUNCHER_PUZZLE_HASH, SINGLETON_TOP_LAYER_PUZZLE_HASH}; +use clvm_utils::{curry_tree_hash, tree_hash_atom, tree_hash_pair}; + +use crate::{ChainedSpend, CreateCoinWithoutMemos, SpendContext, SpendError, SpendableLauncher}; + +pub struct Launcher { + coin: Coin, +} + +impl Launcher { + pub fn new(parent_coin_id: Bytes32, amount: u64) -> Self { + Self { + coin: Coin::new( + parent_coin_id, + SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + amount, + ), + } + } + + pub fn coin(&self) -> Coin { + self.coin + } + + pub fn create(self, ctx: &mut SpendContext) -> Result { + let amount = self.coin.amount; + + Ok(SpendableLauncher::new( + self.coin, + ChainedSpend { + parent_conditions: vec![ctx.alloc(CreateCoinWithoutMemos { + puzzle_hash: SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + amount, + })?], + }, + )) + } +} + +pub fn singleton_struct_hash(launcher_id: Bytes32) -> Bytes32 { + let singleton_hash = tree_hash_atom(&SINGLETON_TOP_LAYER_PUZZLE_HASH); + let launcher_id_hash = tree_hash_atom(&launcher_id); + let launcher_puzzle_hash = tree_hash_atom(&SINGLETON_LAUNCHER_PUZZLE_HASH); + + let pair = tree_hash_pair(launcher_id_hash, launcher_puzzle_hash); + tree_hash_pair(singleton_hash, pair).into() +} + +pub fn singleton_puzzle_hash(launcher_id: Bytes32, inner_puzzle_hash: Bytes32) -> Bytes32 { + curry_tree_hash( + SINGLETON_TOP_LAYER_PUZZLE_HASH, + &[ + singleton_struct_hash(launcher_id).into(), + inner_puzzle_hash.into(), + ], + ) + .into() +} + +#[cfg(test)] +mod tests { + use chia_puzzles::singleton::{ + SingletonArgs, SingletonStruct, SINGLETON_LAUNCHER_PUZZLE_HASH, + SINGLETON_TOP_LAYER_PUZZLE_HASH, + }; + use clvm_utils::CurriedProgram; + use clvmr::Allocator; + + use crate::SpendContext; + + use super::*; + + #[test] + fn test_puzzle_hash() { + let mut allocator = Allocator::new(); + let mut ctx = SpendContext::new(&mut allocator); + + let inner_puzzle = ctx.alloc([1, 2, 3]).unwrap(); + let inner_puzzle_hash = ctx.tree_hash(inner_puzzle); + + let launcher_id = Bytes32::new([34; 32]); + + let singleton_puzzle = ctx.singleton_top_layer(); + + let puzzle = ctx + .alloc(CurriedProgram { + program: singleton_puzzle, + args: SingletonArgs { + singleton_struct: SingletonStruct { + mod_hash: SINGLETON_TOP_LAYER_PUZZLE_HASH.into(), + launcher_id, + launcher_puzzle_hash: SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + }, + inner_puzzle, + }, + }) + .unwrap(); + let allocated_puzzle_hash = ctx.tree_hash(puzzle); + + let puzzle_hash = singleton_puzzle_hash(launcher_id, inner_puzzle_hash.into()); + + assert_eq!(hex::encode(allocated_puzzle_hash), hex::encode(puzzle_hash)); + } +} diff --git a/src/spends/puzzles/singleton/singleton_spend.rs b/src/spends/puzzles/singleton/singleton_spend.rs new file mode 100644 index 00000000..2c0ec314 --- /dev/null +++ b/src/spends/puzzles/singleton/singleton_spend.rs @@ -0,0 +1,41 @@ +use chia_protocol::{Bytes32, Coin, CoinSpend}; +use chia_puzzles::{ + singleton::{ + SingletonArgs, SingletonSolution, SingletonStruct, SINGLETON_LAUNCHER_PUZZLE_HASH, + SINGLETON_TOP_LAYER_PUZZLE_HASH, + }, + Proof, +}; +use clvm_utils::CurriedProgram; + +use crate::{InnerSpend, SpendContext, SpendError}; + +pub fn spend_singleton( + ctx: &mut SpendContext, + coin: Coin, + launcher_id: Bytes32, + lineage_proof: Proof, + inner_spend: InnerSpend, +) -> Result { + let singleton_puzzle = ctx.singleton_top_layer(); + + let puzzle_reveal = ctx.serialize(CurriedProgram { + program: singleton_puzzle, + args: SingletonArgs { + singleton_struct: SingletonStruct { + mod_hash: SINGLETON_TOP_LAYER_PUZZLE_HASH.into(), + launcher_id, + launcher_puzzle_hash: SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + }, + inner_puzzle: inner_spend.puzzle(), + }, + })?; + + let solution = ctx.serialize(SingletonSolution { + lineage_proof, + amount: coin.amount, + inner_solution: inner_spend.solution(), + })?; + + Ok(CoinSpend::new(coin, puzzle_reveal, solution)) +} diff --git a/src/spends/puzzles/singleton/spendable_launcher.rs b/src/spends/puzzles/singleton/spendable_launcher.rs new file mode 100644 index 00000000..0e166417 --- /dev/null +++ b/src/spends/puzzles/singleton/spendable_launcher.rs @@ -0,0 +1,75 @@ +use chia_protocol::{Bytes32, Coin, CoinSpend}; +use chia_puzzles::singleton::LauncherSolution; +use clvm_traits::{clvm_list, ToClvm}; +use clvmr::NodePtr; +use sha2::{digest::FixedOutput, Digest, Sha256}; + +use crate::{ + singleton_puzzle_hash, AssertCoinAnnouncement, ChainedSpend, SpendContext, SpendError, +}; + +#[must_use = "Launcher coins must be spent in order to create the singleton output."] +pub struct SpendableLauncher { + coin: Coin, + chained_spend: ChainedSpend, +} + +impl SpendableLauncher { + pub(crate) fn new(coin: Coin, chained_spend: ChainedSpend) -> Self { + Self { + coin, + chained_spend, + } + } + + pub fn coin(&self) -> Coin { + self.coin + } + + pub fn spend( + self, + ctx: &mut SpendContext, + singleton_inner_puzzle_hash: Bytes32, + key_value_list: T, + ) -> Result<(ChainedSpend, Coin), SpendError> + where + T: ToClvm, + { + let singleton_puzzle_hash = + singleton_puzzle_hash(self.coin.coin_id(), singleton_inner_puzzle_hash); + + let eve_message = ctx.alloc(clvm_list!( + singleton_puzzle_hash, + self.coin.amount, + &key_value_list + ))?; + let eve_message_hash = ctx.tree_hash(eve_message); + + let mut announcement_id = Sha256::new(); + announcement_id.update(self.coin.coin_id()); + announcement_id.update(eve_message_hash); + + let assert_announcement = ctx.alloc(AssertCoinAnnouncement { + announcement_id: Bytes32::new(announcement_id.finalize_fixed().into()), + })?; + + let launcher = ctx.singleton_launcher(); + let puzzle_reveal = ctx.serialize(launcher)?; + + let solution = ctx.serialize(LauncherSolution { + singleton_puzzle_hash, + amount: self.coin.amount, + key_value_list, + })?; + + ctx.spend(CoinSpend::new(self.coin, puzzle_reveal, solution)); + + let mut chained_spend = self.chained_spend; + chained_spend.parent_conditions.push(assert_announcement); + + let singleton_coin = + Coin::new(self.coin.coin_id(), singleton_puzzle_hash, self.coin.amount); + + Ok((chained_spend, singleton_coin)) + } +} diff --git a/src/spends/puzzles/standard.rs b/src/spends/puzzles/standard.rs new file mode 100644 index 00000000..82e37e98 --- /dev/null +++ b/src/spends/puzzles/standard.rs @@ -0,0 +1,142 @@ +use chia_bls::PublicKey; +use chia_protocol::{Coin, CoinSpend}; +use chia_puzzles::standard::{StandardArgs, StandardSolution}; +use clvm_traits::clvm_quote; +use clvm_utils::CurriedProgram; +use clvmr::NodePtr; + +use crate::{Chainable, ChainedSpend, InnerSpend, SpendContext, SpendError}; + +#[derive(Default)] +pub struct StandardSpend { + coin_spends: Vec, + conditions: Vec, +} + +impl StandardSpend { + pub fn new() -> Self { + Self::default() + } + + pub fn inner_spend( + self, + ctx: &mut SpendContext, + synthetic_key: PublicKey, + ) -> Result { + for coin_spend in self.coin_spends { + ctx.spend(coin_spend); + } + + let standard_puzzle = ctx.standard_puzzle(); + + let puzzle = ctx.alloc(CurriedProgram { + program: standard_puzzle, + args: StandardArgs { synthetic_key }, + })?; + + let solution = ctx.alloc(standard_solution(self.conditions))?; + + Ok(InnerSpend::new(puzzle, solution)) + } + + pub fn finish( + self, + ctx: &mut SpendContext, + coin: Coin, + synthetic_key: PublicKey, + ) -> Result<(), SpendError> { + let inner_spend = self.inner_spend(ctx, synthetic_key)?; + let puzzle_reveal = ctx.serialize(inner_spend.puzzle())?; + let solution = ctx.serialize(inner_spend.solution())?; + ctx.spend(CoinSpend::new(coin, puzzle_reveal, solution)); + Ok(()) + } +} + +impl Chainable for StandardSpend { + fn chain(mut self, chained_spend: ChainedSpend) -> Self { + self.conditions.extend(chained_spend.parent_conditions); + self + } + + fn condition(mut self, condition: NodePtr) -> Self { + self.conditions.push(condition); + self + } +} + +/// Constructs a solution for the standard puzzle, given a list of condition. +/// This assumes no hidden puzzle is being used in this spend. +pub fn standard_solution(conditions: T) -> StandardSolution<(u8, T), ()> { + StandardSolution { + original_public_key: None, + delegated_puzzle: clvm_quote!(conditions), + solution: (), + } +} + +#[cfg(test)] +mod tests { + use chia_bls::{sign, Signature}; + use chia_protocol::SpendBundle; + use chia_puzzles::{standard::STANDARD_PUZZLE_HASH, DeriveSynthetic}; + use clvm_utils::ToTreeHash; + use clvmr::Allocator; + + use crate::{testing::SECRET_KEY, CreateCoinWithoutMemos, RequiredSignature, WalletSimulator}; + + use super::*; + + #[tokio::test] + async fn test_standard_spend() -> anyhow::Result<()> { + let sim = WalletSimulator::new().await; + let peer = sim.peer().await; + + let mut allocator = Allocator::new(); + let mut ctx = SpendContext::new(&mut allocator); + + let sk = SECRET_KEY.derive_synthetic(); + let pk = sk.public_key(); + + let puzzle_hash = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { synthetic_key: pk }, + } + .tree_hash() + .into(); + + let parent = sim.generate_coin(puzzle_hash, 1).await.coin; + + StandardSpend::new() + .condition( + ctx.alloc(CreateCoinWithoutMemos { + puzzle_hash, + amount: 1, + }) + .unwrap(), + ) + .finish(&mut ctx, parent, pk)?; + + let coin_spends = ctx.take_spends(); + + let required_signatures = RequiredSignature::from_coin_spends( + &mut allocator, + &coin_spends, + WalletSimulator::AGG_SIG_ME.into(), + )?; + + let mut aggregated_signature = Signature::default(); + + for required in required_signatures { + aggregated_signature += &sign(&sk, required.final_message()); + } + + let ack = peer + .send_transaction(SpendBundle::new(coin_spends, aggregated_signature)) + .await?; + assert_eq!(ack.error, None); + assert_eq!(ack.status, 1); + + Ok(()) + } +} diff --git a/src/spends/spend_builder.rs b/src/spends/spend_builder.rs new file mode 100644 index 00000000..d7e9fbe0 --- /dev/null +++ b/src/spends/spend_builder.rs @@ -0,0 +1,51 @@ +use clvmr::NodePtr; + +pub trait Chainable { + fn condition(self, condition: NodePtr) -> Self; + fn chain(self, other: ChainedSpend) -> Self; + + fn conditions(mut self, conditions: impl IntoIterator) -> Self + where + Self: Sized, + { + for condition in conditions { + self = self.condition(condition); + } + self + } +} + +#[derive(Debug, Default, Clone)] +pub struct ChainedSpend { + pub parent_conditions: Vec, +} + +impl ChainedSpend { + pub fn new() -> Self { + Self::default() + } + + pub fn extend(&mut self, other: ChainedSpend) { + self.parent_conditions.extend(other.parent_conditions); + } +} + +#[derive(Debug, Clone, Copy)] +pub struct InnerSpend { + puzzle: NodePtr, + solution: NodePtr, +} + +impl InnerSpend { + pub fn new(puzzle: NodePtr, solution: NodePtr) -> Self { + Self { puzzle, solution } + } + + pub fn puzzle(&self) -> NodePtr { + self.puzzle + } + + pub fn solution(&self) -> NodePtr { + self.solution + } +} diff --git a/src/spends/spend_context.rs b/src/spends/spend_context.rs new file mode 100644 index 00000000..4b7a9245 --- /dev/null +++ b/src/spends/spend_context.rs @@ -0,0 +1,197 @@ +use std::collections::HashMap; + +use chia_protocol::{CoinSpend, Program}; +use chia_puzzles::{ + cat::{ + CAT_PUZZLE, CAT_PUZZLE_HASH, EVERYTHING_WITH_SIGNATURE_TAIL_PUZZLE, + EVERYTHING_WITH_SIGNATURE_TAIL_PUZZLE_HASH, + }, + did::{DID_INNER_PUZZLE, DID_INNER_PUZZLE_HASH}, + nft::{ + NFT_INTERMEDIATE_LAUNCHER_PUZZLE, NFT_INTERMEDIATE_LAUNCHER_PUZZLE_HASH, + NFT_OWNERSHIP_LAYER_PUZZLE, NFT_OWNERSHIP_LAYER_PUZZLE_HASH, NFT_ROYALTY_TRANSFER_PUZZLE, + NFT_ROYALTY_TRANSFER_PUZZLE_HASH, NFT_STATE_LAYER_PUZZLE, NFT_STATE_LAYER_PUZZLE_HASH, + }, + offer::{SETTLEMENT_PAYMENTS_PUZZLE, SETTLEMENT_PAYMENTS_PUZZLE_HASH}, + singleton::{ + SINGLETON_LAUNCHER_PUZZLE, SINGLETON_LAUNCHER_PUZZLE_HASH, SINGLETON_TOP_LAYER_PUZZLE, + SINGLETON_TOP_LAYER_PUZZLE_HASH, + }, + standard::{STANDARD_PUZZLE, STANDARD_PUZZLE_HASH}, +}; +use clvm_traits::{FromNodePtr, ToNodePtr}; +use clvm_utils::{tree_hash, TreeHash}; +use clvmr::{run_program, serde::node_from_bytes, Allocator, ChiaDialect, NodePtr}; + +use crate::SpendError; + +/// A wrapper around `Allocator` that caches puzzles and simplifies coin spending. +pub struct SpendContext<'a> { + allocator: &'a mut Allocator, + puzzles: HashMap, + coin_spends: Vec, +} + +impl<'a> SpendContext<'a> { + /// Create a new `SpendContext` from an `Allocator` reference. + pub fn new(allocator: &'a mut Allocator) -> Self { + Self { + allocator, + puzzles: HashMap::new(), + coin_spends: Vec::new(), + } + } + + /// Get a reference to the [`Allocator`]. + pub fn allocator(&self) -> &Allocator { + self.allocator + } + + /// Get a mutable reference to the [`Allocator`]. + pub fn allocator_mut(&mut self) -> &mut Allocator { + self.allocator + } + + /// Get a reference to the list of coin spends. + pub fn spends(&self) -> &[CoinSpend] { + &self.coin_spends + } + + /// Take the coin spends out of the [`SpendContext`]. + pub fn take_spends(&mut self) -> Vec { + std::mem::take(&mut self.coin_spends) + } + + /// Add a [`CoinSpend`] to the list. + pub fn spend(&mut self, coin_spend: CoinSpend) { + self.coin_spends.push(coin_spend); + } + + /// Allocate a new node and return its pointer. + pub fn alloc(&mut self, value: T) -> Result + where + T: ToNodePtr, + { + Ok(value.to_node_ptr(self.allocator)?) + } + + /// Extract a value from a node pointer. + pub fn extract(&self, ptr: NodePtr) -> Result + where + T: FromNodePtr, + { + Ok(T::from_node_ptr(self.allocator, ptr)?) + } + + /// Compute the tree hash of a node pointer. + pub fn tree_hash(&self, ptr: NodePtr) -> TreeHash { + tree_hash(self.allocator, ptr) + } + + /// Run a puzzle with a solution and return the result. + pub fn run(&mut self, puzzle: NodePtr, solution: NodePtr) -> Result { + let result = run_program( + self.allocator, + &ChiaDialect::new(0), + puzzle, + solution, + u64::MAX, + )?; + Ok(result.1) + } + + /// Serialize a value and return a `Program`. + pub fn serialize(&mut self, value: T) -> Result + where + T: ToNodePtr, + { + let ptr = value.to_node_ptr(self.allocator)?; + Ok(Program::from_node_ptr(self.allocator, ptr)?) + } + + /// Allocate the standard puzzle and return its pointer. + pub fn standard_puzzle(&mut self) -> NodePtr { + self.puzzle(STANDARD_PUZZLE_HASH, &STANDARD_PUZZLE) + } + + /// Allocate the CAT puzzle and return its pointer. + pub fn cat_puzzle(&mut self) -> NodePtr { + self.puzzle(CAT_PUZZLE_HASH, &CAT_PUZZLE) + } + + /// Allocate the DID inner puzzle and return its pointer. + pub fn did_inner_puzzle(&mut self) -> NodePtr { + self.puzzle(DID_INNER_PUZZLE_HASH, &DID_INNER_PUZZLE) + } + + /// Allocate the NFT intermediate launcher puzzle and return its pointer. + pub fn nft_intermediate_launcher(&mut self) -> NodePtr { + self.puzzle( + NFT_INTERMEDIATE_LAUNCHER_PUZZLE_HASH, + &NFT_INTERMEDIATE_LAUNCHER_PUZZLE, + ) + } + + /// Allocate the NFT royalty transfer puzzle and return its pointer. + pub fn nft_royalty_transfer(&mut self) -> NodePtr { + self.puzzle( + NFT_ROYALTY_TRANSFER_PUZZLE_HASH, + &NFT_ROYALTY_TRANSFER_PUZZLE, + ) + } + + /// Allocate the NFT ownership layer puzzle and return its pointer. + pub fn nft_ownership_layer(&mut self) -> NodePtr { + self.puzzle(NFT_OWNERSHIP_LAYER_PUZZLE_HASH, &NFT_OWNERSHIP_LAYER_PUZZLE) + } + + /// Allocate the NFT state layer puzzle and return its pointer. + pub fn nft_state_layer(&mut self) -> NodePtr { + self.puzzle(NFT_STATE_LAYER_PUZZLE_HASH, &NFT_STATE_LAYER_PUZZLE) + } + + /// Allocate the singleton top layer puzzle and return its pointer. + pub fn singleton_top_layer(&mut self) -> NodePtr { + self.puzzle(SINGLETON_TOP_LAYER_PUZZLE_HASH, &SINGLETON_TOP_LAYER_PUZZLE) + } + + /// Allocate the singleton launcher puzzle and return its pointer. + pub fn singleton_launcher(&mut self) -> NodePtr { + self.puzzle(SINGLETON_LAUNCHER_PUZZLE_HASH, &SINGLETON_LAUNCHER_PUZZLE) + } + + /// Allocate the EverythingWithSignature TAIL puzzle and return its pointer. + pub fn everything_with_signature_tail_puzzle(&mut self) -> NodePtr { + // todo: add constant to chia_rs + self.puzzle( + EVERYTHING_WITH_SIGNATURE_TAIL_PUZZLE_HASH, + &EVERYTHING_WITH_SIGNATURE_TAIL_PUZZLE, + ) + } + + /// Allocate the settlement payments puzzle and return its pointer. + pub fn settlement_payments_puzzle(&mut self) -> NodePtr { + self.puzzle(SETTLEMENT_PAYMENTS_PUZZLE_HASH, &SETTLEMENT_PAYMENTS_PUZZLE) + } + + /// Preload a puzzle into the cache. + pub fn preload(&mut self, puzzle_hash: TreeHash, ptr: NodePtr) { + self.puzzles.insert(puzzle_hash, ptr); + } + + /// Checks whether a puzzle is in the cache. + pub fn get_puzzle(&self, puzzle_hash: &TreeHash) -> Option { + self.puzzles.get(puzzle_hash).copied() + } + + /// Get a puzzle from the cache or allocate a new one. + pub fn puzzle(&mut self, puzzle_hash: TreeHash, puzzle_bytes: &[u8]) -> NodePtr { + if let Some(puzzle) = self.puzzles.get(&puzzle_hash) { + *puzzle + } else { + let puzzle = node_from_bytes(self.allocator, puzzle_bytes).unwrap(); + self.puzzles.insert(puzzle_hash, puzzle); + puzzle + } + } +} diff --git a/src/spends/spend_error.rs b/src/spends/spend_error.rs new file mode 100644 index 00000000..f1e24a25 --- /dev/null +++ b/src/spends/spend_error.rs @@ -0,0 +1,19 @@ +use clvm_traits::{FromClvmError, ToClvmError}; +use clvmr::reduction::EvalErr; +use thiserror::Error; + +/// Errors that can occur when spending a coin. +#[derive(Debug, Error)] +pub enum SpendError { + /// An error occurred while converting to clvm. + #[error("to clvm error: {0}")] + ToClvm(#[from] ToClvmError), + + /// An error occurred while converting from clvm. + #[error("from clvm error: {0}")] + FromClvm(#[from] FromClvmError), + + /// An error occurred while evaluating a program. + #[error("eval error: {0}")] + Eval(#[from] EvalErr), +} diff --git a/src/spends/standard.rs b/src/spends/standard.rs deleted file mode 100644 index 08696564..00000000 --- a/src/spends/standard.rs +++ /dev/null @@ -1,130 +0,0 @@ -use chia_bls::PublicKey; -use chia_protocol::{Coin, CoinSpend}; -use chia_wallet::standard::{StandardArgs, StandardSolution}; -use clvm_traits::{clvm_quote, ToClvm}; -use clvm_utils::CurriedProgram; -use clvmr::NodePtr; - -use crate::{SpendContext, SpendError}; - -/// Constructs a solution for the standard puzzle, given a list of condition. -/// This assumes no hidden puzzle is being used in this spend. -pub fn standard_solution(conditions: T) -> StandardSolution<(u8, T), ()> { - StandardSolution { - original_public_key: None, - delegated_puzzle: clvm_quote!(conditions), - solution: (), - } -} - -/// Creates a new coin spend for a given standard transaction coin. -pub fn spend_standard_coin( - ctx: &mut SpendContext, - coin: Coin, - synthetic_key: PublicKey, - conditions: T, -) -> Result -where - T: ToClvm, -{ - let standard_puzzle = ctx.standard_puzzle(); - - let puzzle_reveal = ctx.serialize(CurriedProgram { - program: standard_puzzle, - args: StandardArgs { synthetic_key }, - })?; - let solution = ctx.alloc(standard_solution(conditions))?; - let serialized_solution = ctx.serialize(solution)?; - - Ok(CoinSpend::new(coin, puzzle_reveal, serialized_solution)) -} - -/// A coin and its corresponding public key. -pub struct StandardSpend { - /// The coin being spent. - pub coin: Coin, - - /// The public key corresponding to the coin. - pub synthetic_key: PublicKey, -} - -/// Spends a set of standard transaction coins. -pub fn spend_standard_coins( - ctx: &mut SpendContext, - standard_spends: Vec, - conditions: T, -) -> Result, SpendError> -where - T: ToClvm, -{ - let mut coin_spends = Vec::new(); - - let conditions = ctx.alloc(conditions)?; - - for (i, spend) in standard_spends.into_iter().enumerate() { - // todo: add announcements - let coin_spend = spend_standard_coin( - ctx, - spend.coin, - spend.synthetic_key, - if i == 0 { conditions } else { NodePtr::NIL }, - )?; - coin_spends.push(coin_spend); - } - - Ok(coin_spends) -} - -#[cfg(test)] -mod tests { - use chia_bls::derive_keys::master_to_wallet_unhardened; - use chia_protocol::Bytes32; - use chia_wallet::{standard::DEFAULT_HIDDEN_PUZZLE_HASH, DeriveSynthetic}; - use clvmr::{serde::node_to_bytes, Allocator}; - use hex_literal::hex; - - use crate::{testing::SECRET_KEY, CreateCoinWithoutMemos}; - - use super::*; - - #[test] - fn test_standard_spend() { - let synthetic_key = master_to_wallet_unhardened(&SECRET_KEY.public_key(), 0) - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH); - - let mut a = Allocator::new(); - let mut ctx = SpendContext::new(&mut a); - - let coin = Coin::new(Bytes32::from([0; 32]), Bytes32::from([1; 32]), 42); - let puzzle_hash = coin.puzzle_hash; - let amount = coin.amount; - - let coin_spend = spend_standard_coin( - &mut ctx, - coin, - synthetic_key, - [CreateCoinWithoutMemos { - puzzle_hash, - amount, - }], - ) - .unwrap(); - let output_ptr = coin_spend - .puzzle_reveal - .run(&mut a, 0, u64::MAX, &coin_spend.solution) - .unwrap() - .1; - let actual = node_to_bytes(&a, output_ptr).unwrap(); - - let expected = hex!( - " - ffff32ffb08584adae5630842a1766bc444d2b872dd3080f4e5daaecf6f762a4 - be7dc148f37868149d4217f3dcc9183fe61e48d8bfffa09744e53c76d9ce3c6b - eb75a3d414ebbec42e31e96621c66b7a832ca1feccceea80ffff33ffa0010101 - 0101010101010101010101010101010101010101010101010101010101ff2a80 - 80 - " - ); - assert_eq!(hex::encode(actual), hex::encode(expected)); - } -} diff --git a/src/sqlite/coin_store.rs b/src/sqlite/coin_store.rs index 71731923..f1faa780 100644 --- a/src/sqlite/coin_store.rs +++ b/src/sqlite/coin_store.rs @@ -189,7 +189,7 @@ mod tests { spent_height: None, }; - upsert_coin_states(&mut conn, vec![coin_state.clone()], None) + upsert_coin_states(&mut conn, vec![coin_state], None) .await .unwrap(); @@ -216,7 +216,7 @@ mod tests { spent_height: None, }; - upsert_coin_states(&mut conn, vec![coin_state.clone()], None) + upsert_coin_states(&mut conn, vec![coin_state], None) .await .unwrap(); @@ -249,7 +249,7 @@ mod tests { spent_height: None, }; - upsert_coin_states(&mut conn, vec![coin_state.clone()], asset_id) + upsert_coin_states(&mut conn, vec![coin_state], asset_id) .await .unwrap(); diff --git a/src/sqlite/key_store.rs b/src/sqlite/key_store.rs index 84a5aa6b..46065a37 100644 --- a/src/sqlite/key_store.rs +++ b/src/sqlite/key_store.rs @@ -1,6 +1,7 @@ use chia_bls::PublicKey; use chia_protocol::Bytes32; -use chia_wallet::standard::standard_puzzle_hash; +use chia_puzzles::standard::{StandardArgs, STANDARD_PUZZLE_HASH}; +use clvm_utils::{CurriedProgram, ToTreeHash}; use sqlx::{Result, SqliteConnection}; /// Get the number of keys in the store. @@ -50,7 +51,15 @@ pub async fn insert_keys( for (i, public_key) in public_keys.iter().enumerate() { let index = index + i as u32; let public_key_bytes = public_key.to_bytes().to_vec(); - let p2_puzzle_hash = standard_puzzle_hash(public_key).to_vec(); + + let p2_puzzle_hash = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { + synthetic_key: *public_key, + }, + } + .tree_hash() + .to_vec(); sqlx::query!( " @@ -148,7 +157,7 @@ pub async fn puzzle_hash_index( /// Get the index of a public key. pub async fn public_key_index( conn: &mut SqliteConnection, - public_key: &PublicKey, + public_key: PublicKey, is_hardened: bool, ) -> Result> { let public_key_bytes = public_key.to_bytes().to_vec(); @@ -174,7 +183,7 @@ mod tests { }, DerivableKey, }; - use chia_wallet::{standard::DEFAULT_HIDDEN_PUZZLE_HASH, DeriveSynthetic}; + use chia_puzzles::DeriveSynthetic; use sqlx::SqlitePool; use crate::testing::SECRET_KEY; @@ -195,21 +204,13 @@ mod tests { // Insert the first batch. let pk_batch_1: Vec = (0..100) - .map(|i| { - intermediate_pk - .derive_unhardened(i) - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH) - }) + .map(|i| intermediate_pk.derive_unhardened(i).derive_synthetic()) .collect(); insert_keys(&mut conn, 0, &pk_batch_1, false).await.unwrap(); // Insert the second batch. let pk_batch_2: Vec = (100..200) - .map(|i| { - intermediate_pk - .derive_unhardened(i) - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH) - }) + .map(|i| intermediate_pk.derive_unhardened(i).derive_synthetic()) .collect(); insert_keys(&mut conn, 100, &pk_batch_2, false) .await @@ -241,17 +242,13 @@ mod tests { // Insert a batch of keys. let pk_batch: Vec = (0..100) - .map(|i| { - intermediate_pk - .derive_unhardened(i) - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH) - }) + .map(|i| intermediate_pk.derive_unhardened(i).derive_synthetic()) .collect(); insert_keys(&mut conn, 0, &pk_batch, false).await.unwrap(); for (i, pk) in pk_batch.into_iter().enumerate() { // Check the index of the key. - let index = public_key_index(&mut conn, &pk, false) + let index = public_key_index(&mut conn, pk, false) .await .unwrap() .unwrap(); @@ -265,7 +262,12 @@ mod tests { assert_eq!(actual, pk); // Ensure the puzzle hash at the index matches. - let ph = standard_puzzle_hash(&pk); + let ph = CurriedProgram { + program: STANDARD_PUZZLE_HASH, + args: StandardArgs { synthetic_key: pk }, + } + .tree_hash(); + let actual = fetch_puzzle_hash(&mut conn, index, false) .await .unwrap() @@ -289,13 +291,9 @@ mod tests { let hardened_sk = master_to_wallet_hardened_intermediate(&SECRET_KEY); // Insert a public key to unhardened and make sure it's not in hardened. - let pk = unhardened_pk - .derive_unhardened(0) - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH); - insert_keys(&mut conn, 0, &[pk.clone()], false) - .await - .unwrap(); - assert!(public_key_index(&mut conn, &pk, true) + let pk = unhardened_pk.derive_unhardened(0).derive_synthetic(); + insert_keys(&mut conn, 0, &[pk], false).await.unwrap(); + assert!(public_key_index(&mut conn, pk, true) .await .unwrap() .is_none()); @@ -304,11 +302,9 @@ mod tests { let pk = hardened_sk .derive_hardened(0) .public_key() - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH); - insert_keys(&mut conn, 0, &[pk.clone()], true) - .await - .unwrap(); - assert!(public_key_index(&mut conn, &pk, false) + .derive_synthetic(); + insert_keys(&mut conn, 0, &[pk], true).await.unwrap(); + assert!(public_key_index(&mut conn, pk, false) .await .unwrap() .is_none()); @@ -321,21 +317,13 @@ mod tests { // Insert a batch of keys. let pk_batch: Vec = (0..100) - .map(|i| { - intermediate_pk - .derive_unhardened(i) - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH) - }) + .map(|i| intermediate_pk.derive_unhardened(i).derive_synthetic()) .collect(); insert_keys(&mut conn, 0, &pk_batch, false).await.unwrap(); // Insert a batch of keys with overlap. let pk_batch: Vec = (50..150) - .map(|i| { - intermediate_pk - .derive_unhardened(i) - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH) - }) + .map(|i| intermediate_pk.derive_unhardened(i).derive_synthetic()) .collect(); insert_keys(&mut conn, 50, &pk_batch, false).await.unwrap(); @@ -348,12 +336,7 @@ mod tests { .await .unwrap() .expect("no public key"); - assert_eq!( - pk, - intermediate_pk - .derive_unhardened(0) - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH) - ); + assert_eq!(pk, intermediate_pk.derive_unhardened(0).derive_synthetic()); // Check the last key. let pk = fetch_public_key(&mut conn, 149, false) @@ -362,9 +345,7 @@ mod tests { .expect("no public key"); assert_eq!( pk, - intermediate_pk - .derive_unhardened(149) - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH) + intermediate_pk.derive_unhardened(149).derive_synthetic() ); } } diff --git a/src/sqlite/puzzle_store.rs b/src/sqlite/puzzle_store.rs index 5e83f518..e6a570df 100644 --- a/src/sqlite/puzzle_store.rs +++ b/src/sqlite/puzzle_store.rs @@ -1,5 +1,6 @@ use chia_protocol::Bytes32; -use chia_wallet::cat::cat_puzzle_hash; +use chia_puzzles::cat::{CatArgs, CAT_PUZZLE_HASH}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; use sqlx::{Result, SqliteConnection}; use super::fetch_derivation_index; @@ -71,8 +72,17 @@ pub async fn extend_cat_puzzle_hashes( .try_into() .unwrap(); - let puzzle_hash = cat_puzzle_hash(asset_id.to_bytes(), p2_puzzle_hash); - puzzle_hashes.push(Bytes32::new(puzzle_hash)); + let puzzle_hash = CurriedProgram { + program: CAT_PUZZLE_HASH, + args: CatArgs { + mod_hash: CAT_PUZZLE_HASH.into(), + tail_program_hash: asset_id, + inner_puzzle: TreeHash::new(p2_puzzle_hash), + }, + } + .tree_hash(); + + puzzle_hashes.push(Bytes32::from(puzzle_hash)); let puzzle_hash = puzzle_hash.to_vec(); let asset_id = asset_id.to_vec(); diff --git a/src/sqlite/transaction_store.rs b/src/sqlite/transaction_store.rs index 557da9f1..ef66cb44 100644 --- a/src/sqlite/transaction_store.rs +++ b/src/sqlite/transaction_store.rs @@ -223,7 +223,7 @@ mod tests { let solution = Program::default(); let coin_spend = CoinSpend { - coin: coin.clone(), + coin, puzzle_reveal, solution, }; @@ -245,7 +245,7 @@ mod tests { // Get the removals and compare. let removals = fetch_removals(&mut conn, transaction_id).await.unwrap(); - assert_eq!(removals, vec![coin.clone()]); + assert_eq!(removals, vec![coin]); // Get all spent coins and make sure the coin is there. let spent_coins = fetch_spent_coins(&mut conn).await.unwrap(); diff --git a/src/wallet.rs b/src/wallet.rs index dcc3c5f9..d71a093f 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,7 +1,9 @@ mod coin_selection; mod required_signature; mod signer; +mod wallet_simulator; pub use coin_selection::*; pub use required_signature::*; pub use signer::*; +pub use wallet_simulator::*; diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index b4da5c7c..7e59977d 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -49,7 +49,7 @@ pub fn select_coins( for coin in spendable_coins.iter() { if coin.amount as u128 == amount { let mut result = HashSet::new(); - result.insert(coin.clone()); + result.insert(*coin); return Ok(result); } } @@ -61,7 +61,7 @@ pub fn select_coins( let coin_amount = coin.amount as u128; if coin_amount < amount { - smaller_coins.insert(coin.clone()); + smaller_coins.insert(*coin); smaller_sum += coin_amount; } } @@ -117,7 +117,7 @@ fn sum_largest_coins(coins: &[Coin], amount: u128) -> HashSet { let mut selected_sum = 0; for coin in coins { selected_sum += coin.amount as u128; - selected_coins.insert(coin.clone()); + selected_coins.insert(*coin); if selected_sum >= amount { return selected_coins; @@ -132,7 +132,7 @@ fn smallest_coin_above(coins: &[Coin], amount: u128) -> Option { } for coin in coins.iter().rev() { if (coin.amount as u128) >= amount { - return Some(coin.clone()); + return Some(*coin); } } unreachable!(); @@ -172,7 +172,7 @@ pub fn knapsack_coin_algorithm( } selected_sum += coin.amount as u128; - selected_coins.insert(coin.clone()); + selected_coins.insert(*coin); if selected_sum == amount { return Some(selected_coins); diff --git a/src/wallet/required_signature.rs b/src/wallet/required_signature.rs index 35b14be1..e6b3f03c 100644 --- a/src/wallet/required_signature.rs +++ b/src/wallet/required_signature.rs @@ -1,11 +1,11 @@ use chia_bls::PublicKey; -use chia_protocol::{Bytes, Coin, CoinSpend, SpendBundle}; +use chia_protocol::{Bytes, Bytes32, Coin, CoinSpend}; use clvm_traits::{FromClvm, FromClvmError}; use clvmr::{allocator::NodePtr, reduction::EvalErr, Allocator}; use sha2::{digest::FixedOutput, Digest, Sha256}; use thiserror::Error; -use crate::{trim_leading_zeros, AggSig, AggSigKind}; +use crate::{u64_to_bytes, AggSig, AggSigKind}; /// An error that occurs while trying to sign a coin spend. #[derive(Debug, Error)] @@ -25,12 +25,12 @@ pub struct RequiredSignature { public_key: PublicKey, raw_message: Bytes, appended_info: Vec, - domain_string: Option<[u8; 32]>, + domain_string: Option, } impl RequiredSignature { /// Converts a known AggSig condition to a `RequiredSignature` if possible. - pub fn from_condition(coin: &Coin, condition: AggSig, agg_sig_me: [u8; 32]) -> Self { + pub fn from_condition(coin: &Coin, condition: AggSig, agg_sig_me: Bytes32) -> Self { let mut hasher = Sha256::new(); hasher.update(agg_sig_me); @@ -90,7 +90,7 @@ impl RequiredSignature { public_key, raw_message: message, appended_info, - domain_string: Some(hasher.finalize_fixed().into()), + domain_string: Some(Bytes32::new(hasher.finalize_fixed().into())), } } @@ -100,7 +100,7 @@ impl RequiredSignature { pub fn from_coin_spend( allocator: &mut Allocator, coin_spend: &CoinSpend, - agg_sig_me: [u8; 32], + agg_sig_me: Bytes32, ) -> Result, ConditionError> { let output = coin_spend .puzzle_reveal @@ -122,21 +122,21 @@ impl RequiredSignature { /// Calculates the required signatures for a spend bundle. /// All of these signatures are aggregated together should /// sufficient, unless secp keys are used as well. - pub fn from_spend_bundle( + pub fn from_coin_spends( allocator: &mut Allocator, - spend_bundle: &SpendBundle, - agg_sig_me: [u8; 32], + coin_spends: &[CoinSpend], + agg_sig_me: Bytes32, ) -> Result, ConditionError> { let mut required_signatures = Vec::new(); - for coin_spend in &spend_bundle.coin_spends { + for coin_spend in coin_spends { required_signatures.extend(Self::from_coin_spend(allocator, coin_spend, agg_sig_me)?); } Ok(required_signatures) } /// The public key required to verify the signature. - pub fn public_key(&self) -> &PublicKey { - &self.public_key + pub fn public_key(&self) -> PublicKey { + self.public_key } /// The message field of the condition, without anything appended. @@ -150,7 +150,7 @@ impl RequiredSignature { } /// The domain string that is appended to the condition's message. - pub fn domain_string(&self) -> Option<[u8; 32]> { + pub fn domain_string(&self) -> Option { self.domain_string } @@ -159,17 +159,12 @@ impl RequiredSignature { let mut message = Vec::from(self.raw_message.as_ref()); message.extend(&self.appended_info); if let Some(domain_string) = self.domain_string { - message.extend(domain_string); + message.extend(domain_string.to_bytes()); } message } } -fn u64_to_bytes(amount: u64) -> Vec { - let bytes: Vec = amount.to_be_bytes().into(); - trim_leading_zeros(bytes.as_slice()).to_vec() -} - #[cfg(test)] mod tests { use crate::testing::SECRET_KEY; @@ -178,15 +173,15 @@ mod tests { use chia_bls::derive_keys::master_to_wallet_unhardened; use chia_protocol::Bytes32; - use chia_wallet::{standard::DEFAULT_HIDDEN_PUZZLE_HASH, DeriveSynthetic}; + use chia_puzzles::DeriveSynthetic; #[test] fn test_messages() { let coin = Coin::new(Bytes32::from([1; 32]), Bytes32::from([2; 32]), 3); - let agg_sig_data = [4u8; 32]; + let agg_sig_data = Bytes32::new([4u8; 32]); - let public_key = master_to_wallet_unhardened(&SECRET_KEY.public_key(), 0) - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH); + let public_key = + master_to_wallet_unhardened(&SECRET_KEY.public_key(), 0).derive_synthetic(); let message: Bytes = vec![1, 2, 3].into(); @@ -256,7 +251,7 @@ mod tests { for (condition, appended_info, domain_string) in cases { let required = RequiredSignature::from_condition(&coin, condition, agg_sig_data); - assert_eq!(required.public_key(), &public_key); + assert_eq!(required.public_key(), public_key); assert_eq!(required.raw_message(), message.as_ref()); assert_eq!(hex::encode(required.appended_info()), appended_info); assert_eq!(required.domain_string().map(hex::encode), domain_string); @@ -265,7 +260,7 @@ mod tests { message.extend(required.raw_message()); message.extend(required.appended_info()); if let Some(domain_string) = required.domain_string() { - message.extend(domain_string); + message.extend(domain_string.to_bytes()); } assert_eq!(hex::encode(message), hex::encode(required.final_message())); diff --git a/src/wallet/signer.rs b/src/wallet/signer.rs index 64a0afc3..a77324d7 100644 --- a/src/wallet/signer.rs +++ b/src/wallet/signer.rs @@ -1,5 +1,5 @@ use chia_bls::{sign, DerivableKey, SecretKey, Signature}; -use chia_wallet::DeriveSynthetic; +use chia_puzzles::DeriveSynthetic; /// Responsible for signing messages. pub trait Signer { @@ -28,7 +28,7 @@ impl Signer for HardenedMemorySigner { let sk = self .intermediate_sk .derive_hardened(index) - .derive_synthetic(&self.hidden_puzzle_hash); + .derive_synthetic_hidden(&self.hidden_puzzle_hash); sign(&sk, message) } } @@ -54,7 +54,7 @@ impl Signer for UnhardenedMemorySigner { let sk = self .intermediate_sk .derive_unhardened(index) - .derive_synthetic(&self.hidden_puzzle_hash); + .derive_synthetic_hidden(&self.hidden_puzzle_hash); sign(&sk, message) } } @@ -63,7 +63,7 @@ impl Signer for UnhardenedMemorySigner { mod tests { use chia_bls::derive_keys::master_to_wallet_unhardened_intermediate; use chia_protocol::{Bytes, Bytes32, Coin, CoinSpend, Program}; - use chia_wallet::standard::DEFAULT_HIDDEN_PUZZLE_HASH; + use chia_puzzles::standard::DEFAULT_HIDDEN_PUZZLE_HASH; use clvm_traits::{clvm_list, FromNodePtr, ToClvm}; use clvmr::{Allocator, NodePtr}; use hex_literal::hex; @@ -101,7 +101,8 @@ mod tests { ) -> Signature { let mut a = Allocator::new(); let required_signatures = - RequiredSignature::from_coin_spend(&mut a, coin_spend, AGG_SIG_ME).unwrap(); + RequiredSignature::from_coin_spend(&mut a, coin_spend, Bytes32::new(AGG_SIG_ME)) + .unwrap(); let mut aggregated_signature = Signature::default(); @@ -134,14 +135,13 @@ mod tests { let intermediate_pk = intermediate_sk.public_key(); let mut conn = pool.acquire().await.unwrap(); - let signer = UnhardenedMemorySigner::new(intermediate_sk, DEFAULT_HIDDEN_PUZZLE_HASH); + let signer = + UnhardenedMemorySigner::new(intermediate_sk, DEFAULT_HIDDEN_PUZZLE_HASH.into()); insert_keys( &mut conn, 0, - &[intermediate_pk - .derive_unhardened(0) - .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH)], + &[intermediate_pk.derive_unhardened(0).derive_synthetic()], false, ) .await diff --git a/src/wallet/wallet_simulator.rs b/src/wallet/wallet_simulator.rs new file mode 100644 index 00000000..afae50eb --- /dev/null +++ b/src/wallet/wallet_simulator.rs @@ -0,0 +1,1022 @@ +use std::{ + collections::{HashMap, HashSet}, + net::SocketAddr, + sync::Arc, +}; + +use chia_bls::aggregate_verify; +use chia_client::Peer; +use chia_consensus::gen::{ + conditions::EmptyVisitor, + owned_conditions::OwnedSpendBundleConditions, + run_block_generator::run_block_generator, + solution_generator::solution_generator, + validation_error::{ErrorCode, ValidationErr}, +}; +use chia_protocol::{ + Bytes, Bytes32, Coin, CoinState, CoinStateUpdate, Message, NewPeakWallet, Program, + ProtocolMessageTypes, PuzzleSolutionResponse, RegisterForCoinUpdates, RegisterForPhUpdates, + RejectPuzzleSolution, RequestChildren, RequestPuzzleSolution, RespondChildren, + RespondPuzzleSolution, RespondToCoinUpdates, RespondToPhUpdates, SendTransaction, SpendBundle, + TransactionAck, +}; +use chia_traits::Streamable; +use clvmr::{Allocator, NodePtr, MEMPOOL_MODE}; +use futures_channel::mpsc::{unbounded, UnboundedSender}; +use futures_util::{future, pin_mut, SinkExt, StreamExt, TryStreamExt}; +use indexmap::{IndexMap, IndexSet}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; +use sha2::{Digest, Sha256}; +use tokio::{ + net::{TcpListener, TcpStream}, + sync::Mutex, + task::JoinHandle, +}; +use tokio_tungstenite::{connect_async, tungstenite::Message as WsMessage}; + +use crate::RequiredSignature; + +type PeerMapInner = HashMap>; +type PeerMap = Arc>; + +/// A very limited full node simulator that can be used to test specifically wallet functionality. +/// It's not guaranteed to be fully accurate, and is only to be able to test wallet code efficiently. +pub struct WalletSimulator { + rng: Mutex, + addr: SocketAddr, + join_handle: JoinHandle<()>, + data: Arc>, +} + +#[derive(Default)] +struct Data { + block_height: u32, + coin_states: IndexMap, + hinted_coins: IndexMap>, + puzzle_subscriptions: IndexMap>, + coin_subscriptions: IndexMap>, + puzzle_and_solutions: IndexMap, +} + +impl WalletSimulator { + /// The `AGG_SIG_ME_ADDIITONAL_DATA` constant for the wallet simulator. + pub const AGG_SIG_ME: [u8; 32] = [42; 32]; + + /// Create a new wallet simulator and start listening for connections in the background. + pub async fn new() -> Self { + let addr = "127.0.0.1:0"; + let peer_map = PeerMap::new(Mutex::new(HashMap::new())); + let try_socket = TcpListener::bind(addr).await; + let listener = try_socket.unwrap_or_else(|_| panic!("failed to bind to `{addr}`")); + let addr = listener.local_addr().unwrap(); + + let data = Arc::new(Mutex::new(Data::default())); + let join_handle = tokio::spawn(listen_for_connections(peer_map, listener, data.clone())); + + Self { + rng: Mutex::new(ChaCha8Rng::seed_from_u64(0)), + addr, + join_handle, + data, + } + } + + /// Resets all of the simulator data to default. + pub async fn reset(&self) { + let mut data = self.data.lock().await; + *data = Data::default(); + } + + /// Generate a new coin with the given puzzle hash and amount. + pub async fn generate_coin(&self, puzzle_hash: Bytes32, amount: u64) -> CoinState { + let mut data = self.data.lock().await; + + let bytes = self.rng.lock().await.gen(); + let parent_coin_info = Bytes32::new(bytes); + + let coin = Coin { + parent_coin_info, + puzzle_hash, + amount, + }; + let coin_id = coin.coin_id(); + let coin_state = CoinState::new(coin, None, Some(data.block_height)); + + data.coin_states.insert(coin_id, coin_state); + coin_state + } + + /// Connects a WebSocket peer to the wallet simulator. + pub async fn peer(&self) -> Peer { + let (ws, _) = connect_async(format!("ws://{}", self.addr)) + .await + .expect("failed to connect to websocket server"); + Peer::new(ws) + } +} + +impl Drop for WalletSimulator { + fn drop(&mut self) { + self.join_handle.abort(); + } +} + +async fn listen_for_connections(peer_map: PeerMap, listener: TcpListener, data: Arc>) { + while let Ok((stream, addr)) = listener.accept().await { + tokio::spawn(handle_connection( + peer_map.clone(), + stream, + addr, + data.clone(), + )); + } +} + +async fn handle_connection( + peer_map: PeerMap, + raw_stream: TcpStream, + addr: SocketAddr, + data: Arc>, +) { + let ws_stream = tokio_tungstenite::accept_async(raw_stream) + .await + .expect("failed to accept websocket connection"); + + let (tx, rx) = unbounded(); + peer_map.lock().await.insert(addr, tx); + + let (sink, stream) = ws_stream.split(); + + let broadcast_incoming = stream.try_for_each(|message| { + let peer_map = peer_map.clone(); + let data = data.clone(); + + async move { + let message = Message::from_bytes(&message.into_data()).unwrap(); + + match message.msg_type { + ProtocolMessageTypes::SendTransaction => { + let tx = SendTransaction::from_bytes(message.data.as_ref()).unwrap(); + let spend_bundle = tx.transaction; + + let transaction_id = spend_bundle.name(); + + let error = process_spend_bundle(peer_map.clone(), data, spend_bundle) + .await + .err(); + + let body = match error { + Some(error) => { + TransactionAck::new(transaction_id, 3, Some(format!("{:?}", error.1))) + } + None => TransactionAck::new(transaction_id, 1, None), + } + .to_bytes() + .unwrap(); + + let response = Message { + msg_type: ProtocolMessageTypes::TransactionAck, + data: body.into(), + id: message.id, + } + .to_bytes() + .unwrap(); + + peer_map + .lock() + .await + .get_mut(&addr) + .unwrap() + .send(response.into()) + .await + .unwrap(); + } + ProtocolMessageTypes::RegisterForCoinUpdates => { + let request = + RegisterForCoinUpdates::from_bytes(message.data.as_ref()).unwrap(); + + let mut coin_states = Vec::new(); + let mut data = data.lock().await; + + for coin_id in request.coin_ids.iter() { + if let Some(coin_state) = data.coin_states.get(coin_id).copied() { + coin_states.push(coin_state); + } + + data.coin_subscriptions + .entry(addr) + .or_default() + .insert(*coin_id); + } + + let response = Message { + msg_type: ProtocolMessageTypes::RespondToCoinUpdates, + data: RespondToCoinUpdates { + coin_ids: request.coin_ids, + min_height: request.min_height, + coin_states, + } + .to_bytes() + .unwrap() + .into(), + id: message.id, + } + .to_bytes() + .unwrap(); + + peer_map + .lock() + .await + .get_mut(&addr) + .unwrap() + .send(response.into()) + .await + .unwrap(); + } + ProtocolMessageTypes::RegisterForPhUpdates => { + let request = RegisterForPhUpdates::from_bytes(message.data.as_ref()).unwrap(); + + let mut coin_states = IndexMap::new(); + let mut data = data.lock().await; + + for (coin_id, coin_state) in data.coin_states.iter() { + if request.puzzle_hashes.contains(&coin_state.coin.puzzle_hash) { + coin_states.insert(*coin_id, data.coin_states[coin_id]); + } + } + + for puzzle_hash in request.puzzle_hashes.iter() { + if let Some(coin_state) = data.coin_states.get(puzzle_hash).copied() { + coin_states.insert(coin_state.coin.coin_id(), coin_state); + } + + if let Some(hinted_coins) = + data.hinted_coins.get(&Bytes::new(puzzle_hash.to_vec())) + { + for coin_id in hinted_coins.iter() { + coin_states.insert(*coin_id, data.coin_states[coin_id]); + } + } + + data.puzzle_subscriptions + .entry(addr) + .or_default() + .insert(*puzzle_hash); + } + + let response = Message { + msg_type: ProtocolMessageTypes::RespondToPhUpdates, + data: RespondToPhUpdates { + puzzle_hashes: request.puzzle_hashes, + min_height: request.min_height, + coin_states: coin_states.into_values().collect(), + } + .to_bytes() + .unwrap() + .into(), + id: message.id, + } + .to_bytes() + .unwrap(); + + peer_map + .lock() + .await + .get_mut(&addr) + .unwrap() + .send(response.into()) + .await + .unwrap(); + } + ProtocolMessageTypes::RequestPuzzleSolution => { + let request = RequestPuzzleSolution::from_bytes(message.data.as_ref()).unwrap(); + let data = data.lock().await; + + let matches_height = data + .coin_states + .get(&request.coin_name) + .map_or(false, |cs| cs.spent_height == Some(request.height)); + + let response = match data.puzzle_and_solutions.get(&request.coin_name).cloned() + { + Some((puzzle, solution)) if matches_height => Message { + msg_type: ProtocolMessageTypes::RespondPuzzleSolution, + data: RespondPuzzleSolution::new(PuzzleSolutionResponse::new( + request.coin_name, + request.height, + puzzle, + solution, + )) + .to_bytes() + .unwrap() + .into(), + id: message.id, + } + .to_bytes() + .unwrap(), + _ => Message { + msg_type: ProtocolMessageTypes::RejectPuzzleSolution, + data: RejectPuzzleSolution::new(request.coin_name, request.height) + .to_bytes() + .unwrap() + .into(), + id: message.id, + } + .to_bytes() + .unwrap(), + }; + + peer_map + .lock() + .await + .get_mut(&addr) + .unwrap() + .send(response.into()) + .await + .unwrap(); + } + ProtocolMessageTypes::RequestChildren => { + let request = RequestChildren::from_bytes(message.data.as_ref()).unwrap(); + let data = data.lock().await; + + let coin_states: Vec = data + .coin_states + .iter() + .filter(|(_, cs)| cs.coin.parent_coin_info == request.coin_name) + .map(|(_, cs)| *cs) + .collect(); + + let response = Message { + msg_type: ProtocolMessageTypes::RespondChildren, + data: RespondChildren::new(coin_states).to_bytes().unwrap().into(), + id: message.id, + } + .to_bytes() + .unwrap(); + + peer_map + .lock() + .await + .get_mut(&addr) + .unwrap() + .send(response.into()) + .await + .unwrap(); + } + _ => unimplemented!( + "unsupported message type for wallet simulator: {:?}", + message.msg_type + ), + } + + Ok(()) + } + }); + + let receive_from_others = rx.map(Ok).forward(sink); + + pin_mut!(broadcast_incoming, receive_from_others); + future::select(broadcast_incoming, receive_from_others).await; + + peer_map.lock().await.remove(&addr); +} + +async fn process_spend_bundle( + peer_map: PeerMap, + data: Arc>, + spend_bundle: SpendBundle, +) -> Result<(), ValidationErr> { + let mut allocator = Allocator::new(); + + let gen = solution_generator( + spend_bundle + .coin_spends + .iter() + .cloned() + .map(|spend| (spend.coin, spend.puzzle_reveal, spend.solution)), + ) + .unwrap(); + + let conds = run_block_generator::<&[u8], EmptyVisitor>( + &mut allocator, + &gen, + &[], + 6_600_000_000, + MEMPOOL_MODE, + )?; + + let conds = OwnedSpendBundleConditions::from(&allocator, conds).unwrap(); + + let cond_puzzle_hashes = conds + .spends + .iter() + .map(|s| s.puzzle_hash) + .collect::>(); + + let bundle_puzzle_hashes = spend_bundle + .coin_spends + .iter() + .map(|s| s.coin.puzzle_hash) + .collect::>(); + + if cond_puzzle_hashes != bundle_puzzle_hashes { + return Err(ValidationErr(NodePtr::NIL, ErrorCode::InvalidSpendBundle)); + } + + let required_signatures = RequiredSignature::from_coin_spends( + &mut allocator, + &spend_bundle.coin_spends, + WalletSimulator::AGG_SIG_ME.into(), + ) + .unwrap(); + + if !aggregate_verify( + &spend_bundle.aggregated_signature, + required_signatures + .into_iter() + .map(|r| (r.public_key(), r.final_message())) + .collect::>(), + ) { + return Err(ValidationErr( + NodePtr::NIL, + ErrorCode::BadAggregateSignature, + )); + } + + let data = &mut data.lock().await; + + let mut removed_coins = IndexMap::new(); + let mut added_coins = IndexMap::new(); + let mut added_hints: IndexMap> = IndexMap::new(); + let mut puzzles_and_solutions = IndexMap::new(); + + for coin_spend in spend_bundle.coin_spends.into_iter() { + puzzles_and_solutions.insert( + coin_spend.coin.coin_id(), + (coin_spend.puzzle_reveal, coin_spend.solution), + ); + } + + // Calculate additions and removals. + for spend in conds.spends.iter() { + for new_coin in spend.create_coin.iter() { + let coin = Coin { + parent_coin_info: spend.coin_id, + puzzle_hash: new_coin.0, + amount: new_coin.1, + }; + + let coin_id = coin.coin_id(); + + let coin_state = CoinState { + coin, + spent_height: None, + created_height: Some(data.block_height), + }; + + added_coins.insert(coin_id, coin_state); + + if let Some(hint) = new_coin.2.clone() { + added_hints.entry(hint).or_default().insert(coin_id); + } + } + + let coin_state = data + .coin_states + .get(&spend.coin_id) + .cloned() + .unwrap_or_else(|| CoinState { + coin: Coin { + parent_coin_info: spend.parent_id, + puzzle_hash: spend.puzzle_hash, + amount: spend.coin_amount, + }, + created_height: Some(data.block_height), + spent_height: None, + }); + + removed_coins.insert(spend.coin_id, coin_state); + } + + // Validate removals. + for (coin_id, coin_state) in removed_coins.iter_mut() { + let height = data.block_height; + + if !data.coin_states.contains_key(coin_id) && !added_coins.contains_key(coin_id) { + return Err(ValidationErr(NodePtr::NIL, ErrorCode::UnknownUnspent)); + } + + if coin_state.spent_height.is_some() { + return Err(ValidationErr(NodePtr::NIL, ErrorCode::DoubleSpend)); + } + + coin_state.spent_height = Some(height); + } + + // Update the coin data. + let mut updates = added_coins.clone(); + updates.extend(removed_coins); + data.block_height += 1; + data.coin_states.extend(updates.clone()); + data.hinted_coins.extend(added_hints.clone()); + data.puzzle_and_solutions.extend(puzzles_and_solutions); + + // Calculate a deterministic but fake header hash. + let mut hasher = Sha256::new(); + hasher.update(data.block_height.to_be_bytes()); + let header_hash = Bytes32::new(hasher.finalize().into()); + + let mut peers = peer_map.lock().await; + + // Send updates to peers. + for (&addr, peer) in peers.iter_mut() { + let mut peer_updates = IndexSet::new(); + + let coin_subscriptions = data + .coin_subscriptions + .get(&addr) + .cloned() + .unwrap_or_default(); + + let puzzle_subscriptions = data + .puzzle_subscriptions + .get(&addr) + .cloned() + .unwrap_or_default(); + + for (hint, coins) in added_hints.iter() { + let Ok(hint) = hint.to_vec().try_into() else { + continue; + }; + let hint = Bytes32::new(hint); + + if puzzle_subscriptions.contains(&hint) { + peer_updates.extend(coins.iter().map(|coin_id| data.coin_states[coin_id])); + } + } + + for coin_id in updates.keys() { + if coin_subscriptions.contains(coin_id) { + peer_updates.insert(data.coin_states[coin_id]); + } + + if puzzle_subscriptions.contains(&data.coin_states[coin_id].coin.puzzle_hash) { + peer_updates.insert(data.coin_states[coin_id]); + } + } + + let new_peak = Message { + msg_type: ProtocolMessageTypes::NewPeakWallet, + id: None, + data: NewPeakWallet::new(header_hash, data.block_height, 0, data.block_height) + .to_bytes() + .unwrap() + .into(), + } + .to_bytes() + .unwrap(); + + peer.send(new_peak.into()).await.unwrap(); + + if !peer_updates.is_empty() { + let update = Message { + msg_type: ProtocolMessageTypes::CoinStateUpdate, + id: None, + data: CoinStateUpdate::new( + data.block_height, + data.block_height, + header_hash, + peer_updates.into_iter().collect(), + ) + .to_bytes() + .unwrap() + .into(), + } + .to_bytes() + .unwrap(); + + peer.send(update.into()).await.unwrap(); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use chia_bls::{sign, Signature}; + use chia_client::PeerEvent; + use chia_protocol::{CoinSpend, SpendBundle}; + use chia_puzzles::DeriveSynthetic; + + use crate::{testing::SECRET_KEY, CreateCoinWithMemos, CreateCoinWithoutMemos, SpendContext}; + + use super::*; + + fn sign_bundle(spend_bundle: &mut SpendBundle) { + let sk = SECRET_KEY.derive_synthetic(); + + let mut a = Allocator::new(); + + let required_signatures = RequiredSignature::from_coin_spends( + &mut a, + &spend_bundle.coin_spends, + Bytes32::new(WalletSimulator::AGG_SIG_ME), + ) + .unwrap(); + + let mut aggregated_signature = Signature::default(); + + for required_signature in required_signatures { + aggregated_signature += &sign(&sk, &required_signature.final_message()); + } + } + + #[tokio::test] + async fn test_coin_lineage_many_blocks() { + let sim = WalletSimulator::new().await; + let peer = sim.peer().await; + + let mut a = Allocator::new(); + let mut ctx = SpendContext::new(&mut a); + + let puzzle = ctx.alloc(1).unwrap(); + let puzzle_hash = ctx.tree_hash(puzzle).into(); + let puzzle_reveal = ctx.serialize(puzzle).unwrap(); + + let mut coin = sim.generate_coin(puzzle_hash, 1000).await.coin; + + for _ in 0..1000 { + let solution = ctx + .serialize([CreateCoinWithoutMemos { + puzzle_hash, + amount: coin.amount - 1, + }]) + .unwrap(); + + let coin_spend = CoinSpend::new(coin, puzzle_reveal.clone(), solution); + + let mut spend_bundle = SpendBundle::new(vec![coin_spend], Signature::default()); + sign_bundle(&mut spend_bundle); + + let transaction_id = spend_bundle.name(); + let ack = peer.send_transaction(spend_bundle.clone()).await.unwrap(); + assert_eq!(ack, TransactionAck::new(transaction_id, 1, None)); + + coin = Coin { + parent_coin_info: coin.coin_id(), + puzzle_hash, + amount: coin.amount - 1, + }; + + let ack = peer.send_transaction(spend_bundle).await.unwrap(); + assert_eq!(ack.txid, transaction_id); + assert_eq!(ack.status, 3); + } + } + + #[tokio::test] + async fn test_spend_unknown_coin() { + let sim = WalletSimulator::new().await; + let peer = sim.peer().await; + + let mut a = Allocator::new(); + let mut ctx = SpendContext::new(&mut a); + + let puzzle = ctx.alloc(1).unwrap(); + let puzzle_hash = ctx.tree_hash(puzzle).into(); + let puzzle_reveal = ctx.serialize(puzzle).unwrap(); + + let solution = ctx + .serialize([CreateCoinWithoutMemos { + puzzle_hash, + amount: 1000, + }]) + .unwrap(); + + let coin_spend = CoinSpend::new( + Coin { + parent_coin_info: Bytes32::new([42; 32]), + puzzle_hash, + amount: 1000, + }, + puzzle_reveal, + solution, + ); + + let mut spend_bundle = SpendBundle::new(vec![coin_spend], Signature::default()); + sign_bundle(&mut spend_bundle); + + let transaction_id = spend_bundle.name(); + let ack = peer.send_transaction(spend_bundle).await.unwrap(); + assert_eq!(ack.txid, transaction_id); + assert_eq!(ack.status, 3); + } + + #[tokio::test] + async fn test_coin_subscriptions() { + let sim = WalletSimulator::new().await; + let peer = sim.peer().await; + + let mut a = Allocator::new(); + let mut ctx = SpendContext::new(&mut a); + + let puzzle = ctx.alloc(1).unwrap(); + let puzzle_hash = ctx.tree_hash(puzzle).into(); + let puzzle_reveal = ctx.serialize(puzzle).unwrap(); + + let mut cs = sim.generate_coin(puzzle_hash, 1000).await; + + // Subscribe and request initial state. + let results = peer + .register_for_coin_updates(vec![cs.coin.coin_id()], 0) + .await + .unwrap(); + + assert_eq!(results, vec![cs]); + + // The initial state should still be the same. + let results = peer + .register_for_coin_updates(vec![cs.coin.coin_id()], 0) + .await + .unwrap(); + + assert_eq!(results, vec![cs]); + + let mut receiver = peer.receiver().resubscribe(); + + while receiver.try_recv().is_ok() {} + + // Spend the coin. + let solution = ctx + .serialize([CreateCoinWithoutMemos { + puzzle_hash, + amount: cs.coin.amount - 1, + }]) + .unwrap(); + + let coin_spend = CoinSpend::new(cs.coin, puzzle_reveal, solution); + + let mut spend_bundle = SpendBundle::new(vec![coin_spend], Signature::default()); + sign_bundle(&mut spend_bundle); + + let transaction_id = spend_bundle.name(); + let ack = peer.send_transaction(spend_bundle).await.unwrap(); + assert_eq!(ack, TransactionAck::new(transaction_id, 1, None)); + + // We should have gotten a new peak and an update. + // But the coin is spent now. + cs.spent_height = Some(0); + + let event = receiver.recv().await.unwrap(); + assert!(matches!(event, PeerEvent::NewPeakWallet(..))); + + let event = receiver.recv().await.unwrap(); + match event { + PeerEvent::CoinStateUpdate(update) => { + assert_eq!(update.items, vec![cs]); + } + _ => panic!("unexpected event: {:?}", event), + } + } + + #[tokio::test] + async fn test_puzzle_subscriptions() { + let sim = WalletSimulator::new().await; + let peer = sim.peer().await; + + let mut a = Allocator::new(); + let mut ctx = SpendContext::new(&mut a); + + let puzzle = ctx.alloc(1).unwrap(); + let puzzle_hash = ctx.tree_hash(puzzle).into(); + let puzzle_reveal = ctx.serialize(puzzle).unwrap(); + + let mut cs = sim.generate_coin(puzzle_hash, 1000).await; + + // Subscribe and request initial state. + let results = peer + .register_for_ph_updates(vec![cs.coin.puzzle_hash], 0) + .await + .unwrap(); + + assert_eq!(results, vec![cs]); + + // The initial state should still be the same. + let results = peer + .register_for_ph_updates(vec![cs.coin.puzzle_hash], 0) + .await + .unwrap(); + + assert_eq!(results, vec![cs]); + + let mut receiver = peer.receiver().resubscribe(); + + while receiver.try_recv().is_ok() {} + + // Spend the coin. + let solution = ctx + .serialize([CreateCoinWithoutMemos { + puzzle_hash, + amount: cs.coin.amount - 1, + }]) + .unwrap(); + + let coin_spend = CoinSpend::new(cs.coin, puzzle_reveal, solution); + + let mut spend_bundle = SpendBundle::new(vec![coin_spend], Signature::default()); + sign_bundle(&mut spend_bundle); + + let transaction_id = spend_bundle.name(); + let ack = peer.send_transaction(spend_bundle).await.unwrap(); + assert_eq!(ack, TransactionAck::new(transaction_id, 1, None)); + + // We should have gotten a new peak and an update. + // But the coin is spent now. + cs.spent_height = Some(0); + let new_cs = CoinState { + coin: Coin { + parent_coin_info: cs.coin.coin_id(), + puzzle_hash, + amount: cs.coin.amount - 1, + }, + created_height: Some(0), + spent_height: None, + }; + + let event = receiver.recv().await.unwrap(); + assert!(matches!(event, PeerEvent::NewPeakWallet(..))); + + let event = receiver.recv().await.unwrap(); + match event { + PeerEvent::CoinStateUpdate(update) => { + let items = update.items.into_iter().collect::>(); + let expected = vec![cs, new_cs].into_iter().collect::>(); + assert_eq!(items, expected); + } + _ => panic!("unexpected event: {:?}", event), + } + } + + #[tokio::test] + async fn test_hint_subscriptions() { + let sim = WalletSimulator::new().await; + let peer = sim.peer().await; + + let mut a = Allocator::new(); + let mut ctx = SpendContext::new(&mut a); + + let puzzle = ctx.alloc(1).unwrap(); + let puzzle_hash = ctx.tree_hash(puzzle).into(); + let puzzle_reveal = ctx.serialize(puzzle).unwrap(); + + let cs = sim.generate_coin(puzzle_hash, 1000).await; + + let hint = Bytes32::new([34; 32]); + + // Subscribe and request initial state. + let results = peer.register_for_ph_updates(vec![hint], 0).await.unwrap(); + assert!(results.is_empty()); + + let mut receiver = peer.receiver().resubscribe(); + + while receiver.try_recv().is_ok() {} + + // Spend the coin. + let solution = ctx + .serialize([CreateCoinWithMemos { + puzzle_hash, + amount: cs.coin.amount - 1, + memos: vec![hint.to_bytes().to_vec().into()], + }]) + .unwrap(); + + let coin_spend = CoinSpend::new(cs.coin, puzzle_reveal, solution); + + let mut spend_bundle = SpendBundle::new(vec![coin_spend], Signature::default()); + sign_bundle(&mut spend_bundle); + + let transaction_id = spend_bundle.name(); + let ack = peer.send_transaction(spend_bundle).await.unwrap(); + assert_eq!(ack, TransactionAck::new(transaction_id, 1, None)); + + // We should have gotten a new peak and an update. + let event = receiver.recv().await.unwrap(); + assert!(matches!(event, PeerEvent::NewPeakWallet(..))); + + let event = receiver.recv().await.unwrap(); + match event { + PeerEvent::CoinStateUpdate(update) => { + let new_cs = CoinState { + coin: Coin { + parent_coin_info: cs.coin.coin_id(), + puzzle_hash, + amount: cs.coin.amount - 1, + }, + created_height: Some(0), + spent_height: None, + }; + + assert_eq!(update.items, vec![new_cs]); + } + _ => panic!("unexpected event: {:?}", event), + } + } + + #[tokio::test] + async fn test_puzzle_solution() { + let sim = WalletSimulator::new().await; + let peer = sim.peer().await; + + let mut a = Allocator::new(); + let mut ctx = SpendContext::new(&mut a); + + let puzzle = ctx.alloc(1).unwrap(); + let puzzle_hash = ctx.tree_hash(puzzle).into(); + let puzzle_reveal = ctx.serialize(puzzle).unwrap(); + + let cs = sim.generate_coin(puzzle_hash, 1000).await; + + let solution = ctx + .serialize([CreateCoinWithoutMemos { + puzzle_hash, + amount: cs.coin.amount - 1, + }]) + .unwrap(); + + let coin_spend = CoinSpend::new(cs.coin, puzzle_reveal.clone(), solution.clone()); + + let mut spend_bundle = SpendBundle::new(vec![coin_spend], Signature::default()); + sign_bundle(&mut spend_bundle); + + let transaction_id = spend_bundle.name(); + let ack = peer.send_transaction(spend_bundle).await.unwrap(); + assert_eq!(ack, TransactionAck::new(transaction_id, 1, None)); + + let mut receiver = peer.receiver().resubscribe(); + + while receiver.try_recv().is_ok() {} + + let response = peer + .request_puzzle_and_solution(cs.coin.coin_id(), 0) + .await + .unwrap(); + + assert_eq!( + response, + PuzzleSolutionResponse::new(cs.coin.coin_id(), 0, puzzle_reveal, solution) + ); + } + + #[tokio::test] + async fn test_request_children() { + let sim = WalletSimulator::new().await; + let peer = sim.peer().await; + + let mut a = Allocator::new(); + let mut ctx = SpendContext::new(&mut a); + + let puzzle = ctx.alloc(1).unwrap(); + let puzzle_hash = ctx.tree_hash(puzzle).into(); + let puzzle_reveal = ctx.serialize(puzzle).unwrap(); + + let cs = sim.generate_coin(puzzle_hash, 1000).await; + + let solution = ctx + .serialize([CreateCoinWithoutMemos { + puzzle_hash, + amount: cs.coin.amount - 1, + }]) + .unwrap(); + + let coin_spend = CoinSpend::new(cs.coin, puzzle_reveal.clone(), solution.clone()); + + let mut spend_bundle = SpendBundle::new(vec![coin_spend], Signature::default()); + sign_bundle(&mut spend_bundle); + + let transaction_id = spend_bundle.name(); + let ack = peer.send_transaction(spend_bundle).await.unwrap(); + assert_eq!(ack, TransactionAck::new(transaction_id, 1, None)); + + let mut receiver = peer.receiver().resubscribe(); + + while receiver.try_recv().is_ok() {} + + let response = peer.request_children(cs.coin.coin_id()).await.unwrap(); + + let new_cs = CoinState { + coin: Coin { + parent_coin_info: cs.coin.coin_id(), + puzzle_hash, + amount: cs.coin.amount - 1, + }, + created_height: Some(0), + spent_height: None, + }; + + assert_eq!(response, vec![new_cs]); + } +}