From 988426e4853262bc5717a876f18811a4b7818f52 Mon Sep 17 00:00:00 2001 From: Hossein Zoda Date: Mon, 27 Apr 2026 02:02:38 +0300 Subject: [PATCH] ido, compiles, with claude generated unittests --- Cargo.lock | 202 ++- Cargo.toml | 5 +- internal/config_specification.toml | 6 + src/db/ido/mod.rs | 2481 ++++++++++++++++++---------- src/db/init.rs | 2 +- src/db/mod.rs | 1 + src/index.rs | 1 + src/main.rs | 17 + src/rpc/ido.rs | 157 ++ src/rpc/mod.rs | 1 + src/utiltest.rs | 2 + 11 files changed, 1972 insertions(+), 903 deletions(-) create mode 100644 src/rpc/ido.rs diff --git a/Cargo.lock b/Cargo.lock index 90464cb..27cfa01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -408,6 +408,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link 0.1.1", ] @@ -549,6 +550,40 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.101", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.101", +] + [[package]] name = "data-encoding" version = "2.10.0" @@ -568,11 +603,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -643,6 +679,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.15.0" @@ -965,7 +1007,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -1024,6 +1066,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-conservative" @@ -1330,6 +1375,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1351,6 +1402,17 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -1645,6 +1707,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1663,9 +1736,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-integer" @@ -2234,6 +2307,8 @@ dependencies = [ "hex", "log", "malachite", + "num-bigint", + "num-traits", "rand 0.8.5", "rayon", "reqwest", @@ -2244,6 +2319,7 @@ dependencies = [ "rust_decimal_macros", "serde", "serde_json", + "serde_with", "sqlx", "stderrlog", "tokio", @@ -2306,7 +2382,7 @@ dependencies = [ "either", "figment", "futures", - "indexmap", + "indexmap 2.9.0", "log", "memchr", "multer", @@ -2338,7 +2414,7 @@ checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" dependencies = [ "devise", "glob", - "indexmap", + "indexmap 2.9.0", "proc-macro2", "quote", "rocket_http", @@ -2375,7 +2451,7 @@ dependencies = [ "futures", "http 0.2.12", "hyper 0.14.32", - "indexmap", + "indexmap 2.9.0", "log", "memchr", "pear", @@ -2527,6 +2603,30 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -2567,18 +2667,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2587,14 +2697,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -2618,6 +2729,37 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.9.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2758,7 +2900,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.3", "hashlink", - "indexmap", + "indexmap 2.9.0", "log", "memchr", "once_cell", @@ -2963,6 +3105,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -3071,30 +3219,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -3224,7 +3372,7 @@ version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ - "indexmap", + "indexmap 2.9.0", "serde", "serde_spanned", "toml_datetime", @@ -4070,3 +4218,9 @@ dependencies = [ "quote", "syn 2.0.101", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 6d9a1d7..4fd2d41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,9 @@ anyhow = { version = "1.0.94", features = ["backtrace"] } bitcoincash = "0.32.2" bitcoin_hashes = { version = "0.14.100", default-features = false } # Same version as used by bitcoincash electrum-client-netagnostic = { version = "0.21.2", features = ["use-rustls", "use-websocket"] } -hex = "0.4.3" +hex = { version = "0.4.3", features = ["serde"] } +num-bigint = { version = "0.4", features = ["serde"] } +num-traits = "0.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.133" rocket = { version = "0.5.1", features = ["json"] } @@ -33,6 +35,7 @@ reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-feature rand = "0.8.5" configure_me = "0.4.0" futures = "0.3.32" +serde_with = { version = "3.18.0", features = ["hex"] } [build-dependencies] configure_me_codegen = "0.4.8" diff --git a/internal/config_specification.toml b/internal/config_specification.toml index 4adb4cf..9cabe37 100644 --- a/internal/config_specification.toml +++ b/internal/config_specification.toml @@ -57,3 +57,9 @@ name = "ido_read_slots" type = "u32" doc = "Number of read connection pool slots for the Moria lending database (default: 8)" default = "8" + +[[param]] +name = "debug" +type = "bool" +doc = "Enable debug RPC endpoints (e.g. txchain inspection). Do not use in production." +default = "false" diff --git a/src/db/ido/mod.rs b/src/db/ido/mod.rs index 38d0153..c6cb6c6 100644 --- a/src/db/ido/mod.rs +++ b/src/db/ido/mod.rs @@ -1,115 +1,138 @@ -use log::{debug, info, warn}; +#![allow(non_snake_case)] +use anyhow::Result; +use log::{info}; use num_bigint::{BigInt, Sign}; -use bitcoincash::blockdata::script::{Builder, Script}; +use num_traits::{Zero, ToPrimitive}; +use bitcoincash::blockdata::script::{Builder, Script, Instruction}; +use bitcoincash::blockdata::opcodes; use bitcoin_hashes::sha256d; -use bitcoin_hashes::hash160; +//NOTUSED +//use bitcoin_hashes::hash160; +use bitcoin_hashes::Hash; use serde::{Deserialize, Serialize}; -use serde_json::json; -use bitcoincash::blockdata::transaction::{TxOut, OutPoint}; -use bitcoincash::{PubkeyHash}; -use riftenlabs_defi::cauldron::{get_lockingscript_for_pkh}; +use serde_with::serde_as; +use bitcoincash::blockdata::transaction::Transaction; +use bitcoincash::{Txid, BlockHash, TokenID, Network}; +use std::sync::LazyLock; +use std::collections::{HashMap}; +use sqlx::{Row, SqlitePool}; +use crate::db::blob::{ToBlob, blob_to_display_hex}; -let ITEM_TYPE_BITS: u8 = 0x0f; -let DISTRIBUTOR_FLAG_IS_REFUND: u8 = 0x10; -let CONFIRMATION_NFT_FLAG_IS_REFUND: u8 = 0x10; -//! 0b00000000 -let ITEM_TYPE_OFFERING: u8 = 0x00; -//! 0b00000001 -let ITEM_TYPE_LAUNCHER: u8 = 0x01; -//! 0b00000010 -let ITEM_TYPE_DISTRIBUTOR: u8 = 0x02; -//! 0b00000101 -let ITEM_TYPE_CONFIRMATION_NFT: u8 = 0x05; +const ITEM_TYPE_BITS: u8 = 0x0f; +const DISTRIBUTOR_FLAG_IS_REFUND: u8 = 0x10; +const CONFIRMATION_NFT_FLAG_IS_REFUND: u8 = 0x10; +// 0b00000000 +const ITEM_TYPE_OFFERING: u8 = 0x00; +//UNUSED 0b00000001 +//const ITEM_TYPE_LAUNCHER: u8 = 0x01; +// 0b00000010 +const ITEM_TYPE_DISTRIBUTOR: u8 = 0x02; +// 0b00000101 +const ITEM_TYPE_CONFIRMATION_NFT: u8 = 0x05; -let PERMANENT_LIQUIDITY_SHARE_DENOMINATOR = BigInt::from(100_000_000); -let ONE_YEAR_TIMEVAL = BigInt::from(31536000n) -let ANNUAL_RATE_DENOMINATOR = BigInt(100_000_000); -let PLATFORM_FEE_DENOMINATOR = BigInt::from(100_000_000); -let PRICE_DENOMINATOR = BigInt::from(100_000_000_000); +static PERMANENT_LIQUIDITY_SHARE_DENOMINATOR: LazyLock = LazyLock::new(|| BigInt::from(100_000_000i64)); +// UNUSED +//static ONE_YEAR_TIMEVAL: LazyLock = LazyLock::new(|| BigInt::from(31536000i64)); +// UNUSED +//static ANNUAL_RATE_DENOMINATOR: LazyLock = LazyLock::new(|| BigInt::from(100_000_000i64)); +// UNUSED +//static PLATFORM_FEE_DENOMINATOR: LazyLock = LazyLock::new(|| BigInt::from(100_000_000i64)); +// UNUSED +//static PRICE_DENOMINATOR: LazyLock = LazyLock::new(|| BigInt::from(100_000_000_000i64)); // below should be euqal // PERMANENT_LIQUIDITY_SHARE_DENOMINATOR == ANNUAL_RATE_DENOMINATOR -let MIN_PLP_SHARE = BigInt::from(20_000_000); // 20% -let MAX_PLP_SHARE = BigInt::from(80_000_000); // 80% -let MAX_PLP_ANNUAL_DISCOUNT = BigInt::From(10_000_000); // 10% -let MIN_PLP_ANNUAL_DISCOUNT = BigInt::ZERO; // 0% +static MIN_PLP_SHARE: LazyLock = LazyLock::new(|| BigInt::from(20_000_000i64)); // 20% +static MAX_PLP_SHARE: LazyLock = LazyLock::new(|| BigInt::from(80_000_000i64)); // 80% +static MAX_PLP_ANNUAL_DISCOUNT: LazyLock = LazyLock::new(|| BigInt::from(10_000_000i64)); // 10% +static MIN_PLP_ANNUAL_DISCOUNT: LazyLock = LazyLock::new(|| BigInt::from(0i64)); // 0% -let ENTRY_EXECUTION_FEE = BigInt::from(10000); // 10k sats -let IDO_CREATE_EXECUTION_FEE = BigInt::from(30000); // 30k sats -let PLATFORM_FEE = BigInt::from(10_000_000); // 10% -let DELPHI_TOKEN_ID: &[u8; 32] = &[ +static ENTRY_EXECUTION_FEE: LazyLock = LazyLock::new(|| BigInt::from(10000i64)); // 10k sats +static IDO_CREATE_EXECUTION_FEE: LazyLock = LazyLock::new(|| BigInt::from(30000i64)); // 30k sats +static PLATFORM_FEE: LazyLock = LazyLock::new(|| BigInt::from(10_000_000i64)); // 10% +const DELPHI_TOKEN_ID: &[u8; 32] = &[ 0xd0, 0xd4, 0x6f, 0x5c, 0xbd, 0x82, 0x18, 0x8a, 0xce, 0xde, 0x0d, 0x3e, 0x49, 0xc7, 0x57, 0x00, 0xc1, 0x9c, 0xb8, 0x33, 0x1a, 0x30, 0x10, 0x1f, 0x0b, 0xb6, 0xa2, 0x60, 0x06, 0x6a, 0xc9, 0x72 ]; // TODO:: set the platform fee nfth -let PLATFORM_FEE_NFTH: &[u8; 32] = &[ - 0x00, 0x00 +const CHIPNET_PLATFORM_FEE_NFTH: &[u8; 32] = &[ + 0x07, 0x2d, 0x5f, 0xbe, 0xcf, 0xa1, 0xbc, 0x37, + 0xa1, 0x3f, 0x58, 0xb1, 0x88, 0xf5, 0x0a, 0x5b, + 0xc1, 0xdf, 0xb9, 0xab, 0x8a, 0xe0, 0xbf, 0x5d, + 0x3d, 0x9a, 0x63, 0xf6, 0xf8, 0x1e, 0xfc, 0x0b, +]; +const MAINNET_PLATFORM_FEE_NFTH: &[u8; 32] = &[ + 0x4a, 0xa4, 0xe6, 0x09, 0xb3, 0xfb, 0x97, 0x3b, + 0x44, 0x10, 0x9a, 0x9c, 0x08, 0x4e, 0x75, 0xdc, + 0xc8, 0xe2, 0x6e, 0x1f, 0x7f, 0x8e, 0xfa, 0xc2, + 0x0c, 0x80, 0xb8, 0x94, 0x29, 0x42, 0x13, 0x0e, ]; - -let IDO_SIGNATURE = vec[ +const IDO_SIGNATURE: &[u8] = &[ 0x12, 0x43, 0x61, 0x75, 0x6c, 0x64, 0x72, 0x6f, 0x6e, 0x49, 0x64, 0x6f, 0x2d, 0x32, 0x30, 0x32, 0x36, 0x51, 0x32, 0x75, ]; -let OFFERING_CONTRACT = hex::decode("c0ce01207f75c0519c637600ce8800cf517f755f845188675879827701209dc0009d00d000d394765479a269765579950500e876481796005c7900a063577900a069587900a0695c79587995048033e1015a7995a169785d7995587995048033e1010400e1f5059596776e947b757c7800a0696854790087535e7900a06376608577687863760120857768005f7900a0635f795680547958807e77686e7e51d28851d15779517e8851d356799d02aa20012060797e5e797eaa7e01877e51cd8852796351cc5579a26952d100876452d101207f75577987916968c4539d6752d158798852d35579a26902aa20525152807e5f797eaa7e01877e52cd8853d100876453d101207f7557798791696854d100876454d101207f75577987916968c4559d6800cf517f77547f758100cc00c6527993a26900cd00c78800cf557f77547f758100cf557f75788b54807e00d28800ce00d1886d6d6d6d686d6d6d6d6d51").expect("Decoding failed"); -let OFFERING_ENTRY_CONTRACT = hex::decode("827701209dc0519dc0ce01207f7500ce01207f758800cf517f755f845288c0cf517f7501208401008763c0c878c88876c9529d687551").expect("Decoding failed"); -let LAUNCHER_CONTRACT = hex::decode("0xc0009d00c852c88852c9529d00c854c88854c9549d00c855c88855c9559d00ce01207f7551ce78527e8851cf517f755f8401008853ce01207f7555798853cf567f75817600a06953d100876453d101207f7552798791696851d0547aa07651d0557aa09b63537952799f696851cf557f77547f758100a16302aa2001205d797e57797eaa7e01877e556085537956807e0058807e52d058807e0058807e00d28800d154798800d3009d00cd788851cd788851cc52c6a26951d152ce8851d352d09d51d2008852d10088c4549d75675279517e00d18800d3009d766355ca827755ca7853947f77527f758155ca527953945279947f77787f75526085557956807e51cf557f77547f757e0058807e52d058807e0058807e00d28802aa205979827751805a797e01207e60797e52797eaa7e01877e00cd8802aa20012060797e5a797eaa7e01877e52cd8852cc52c6a26952d152ce8852d352d09d52d2008854d10088c4559d6d756754ca827754ca7853947f77527f758154ca527953945279947f77787f7551d07600a06354d152ce8854d3789d54d2008802aa2001200111797e5b797eaa7e01877e54cd8855d10088c4569d6754d10088c4559d6852567956807e51cf557f77547f757e0058807e7858807e0058807e00d28802aa205c79827751805d797e5c79827751807e5c797e5b79827751807e5b797e01207e5a797e547e5f797e01207e60797e01207e0111797e53797eaa7e01877e00cd8852d152ce8852d352d05279949d52cc52c6a26902aa20520052807e5d797eaa7e01877e52cd8852d200886d6d6802aa2056798277518057797e5a797eaa7e01877e51cd88545b797e51d28851cc51c6a26951d153798851d3009d686d6d6d6d6d6d51").expect("Decoding failed"); -let DISTRIBUTOR_DEPLOY_CONTRACT = hex::decode("c0009d00c852c88852c9529d00cf517f77567f758100ce01207f7551ce01207f75788851cf517f755f84538851cf517f7551ca51ca82770136940120947f7701207f750000537960840100876451cf517f77567f75817b757c51cf577f77587f758177687800a2697600a26952d352d051d0949d587a810000567901208401008764527900a26951c6765479950400e1f50596537a757c6b7c6c765379947b757c53cc5279a26953d1008854cc5379a26954d10088756753d05379950400e1f505967b757c53d0527994777600a06353d3789d53d153ce886753d10088687800a06354d352799d54d153ce886754d100886868547900a06302aa20012057797e5f797eaa7e01877e51cd8851d1587988565551807e5c797e597956799356807e51d28851d3009d55d351d09d55d152ce8802aa20525152807e60797eaa7e01877e55cd8855d200886751d351d09d51d152ce8802aa20012057797e5e797eaa7e01877e51cd8851d20088687600a06302aa2001205b797e5e797eaa7e01877e53cd886753cd016a88687c00a06302aa2001205b797e5d797eaa7e01877e54cd886754cd016a886800cf577f77547f75817651a06300cf517f7500cf517f77567f757e788c54807e00cf5b7f77587f758153799358807e00cf01137f77587f757e00cf011b7f77587f758155799358807e00d28800ce00d18800c700cd8800d000d39d52cf52d28852ce52d18852c752cd88675500cf517f77567f757e00cf5b7f77587f758153799358807e00cf01137f77587f757e00cf011b7f77587f758155799358807e00d28800d158798800d3009d02aa2001205b797e5e797eaa7e01877e00cd8852d100885457790120840100876475536876ce59798876cf517f755f845488756855557a00a063755668c4789e6376d10088c4788b9d686d6d6d6d6d6d6d7551").expect("Decoding failed"); -let DISTRIBUTOR_REFUND_CONTRACT = hex::decode("c0009d00ce01207f7551ce01207f75788851cf517f755f84538851ca51ca82770136940120947f7701207f7551cf517f75760120840100876451cc51c6a26951d100886751d352d09d51d152ce886802aa200120537a7e55797eaa7e01877e51cd8800cf577f77547f75817651a06300cf517f7500cf517f77567f757e788c54807e00cf5b7f77587f757e00cf01137f77587f757e00cf011b7f77587f757e00d28800ce00d18800c700cd88c4529e6352d10088c4539d686755608500cf517f77567f757e00cf5b7f77587f757e00cf01137f77587f757e00cf011b7f77587f757e00d288527900d18800d3009d02aa20012055797e56797eaa7e01877e00cd8852d100885352790120840100876475526876ce54798876cf517f755f845488c4539e6353d10088c4549d6875686d6d7551").expect("Decoding failed"); -let EXECUTION_FEE_PAYOUT_CONTRACT = hex::decode("c0ce00ce01207f758800cf517f755f84528800cf577f77547f758151a169c0cf517f7701207f7502aa2001207b7e7b7eaa7e01877e52cd8852ccc0c6a2").expect("Decoding failed"); -let OFFERING_INITIATOR_CONTRACT = hex::decode("c0c9009dc0c85a79c8885979c9567a9d5879ce827701209d557900a263c0c85a79c8885979c956799d68c0c800d1788800d2578800d3009d00cd597a88c08bc0c878c88876c9547a9d76ca827778ca7853947f77527f75817bca7b53945279947f777c7f75c05293c0c878c88876c9557a9d76ca827778ca7853947f77527f75817bca7b53945279947f777c7f75c05393c0c878c88876c9567a9d76ca827778ca7853947f77527f75815178529302ff00a063755367785293014ba063755268685379ca537a5394537a947b947f7702aa20525352807e5a797e7b7eaa7e01877e54cd8854cc7cc6a26954d10088c05493c0c878c88876c9567a9d76ca827778ca7853947f77527f75815178529302ff00a063755367785293014ba063755268685379ca537a5394537a947b947f7702aa20525352807e59797e7b7eaa7e01877e55cd8855cc7cc6a26955d100885679d07600a0690100557a7e04000000007e51d28851d15479527e8851d3789d51cd02aa20547aaa7e01877e885153d28853d153798853d3009d53cd02aa20537aaa7e01877e8802aa20525352807e557a7eaa7e01877e52cd8852cc5479c6a26952d1547ace8852d39d52d2008856d100876456d101207f75788791696856cd567f75066a0442434d52879169c4579e6357d100876457d101207f75788791696857cd567f75066a0442434d52879169c4589e6358d100876458d101207f75788791696858cd567f75066a0442434d52879169c4599e6359d100876459d101207f75788791696859cd567f75066a0442434d52879169c45a9e635ad10087645ad101207f7578879169685acd567f75066a0442434d52879169c45b9d686868686d7551").expect("Decoding failed"); -let OFFERING_INTEGRATED_TIMELOCKED_P2NFTH_CONTRACT = hex::decode("c0cf527f7701207f75c0cf01227f77815479ce01207f757b88537acf567f75817600a0699f6978ce7bcf7eaa88c0cf517f77517f7581c0c85279c8887cc99c").expect("Decoding failed"); +static OFFERING_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("c0ce01207f75c0519c637600ce8800cf517f755f845188675879827701209dc0009d00d000d394765479a269765579950500e876481796005c7900a063577900a069587900a0695c79587995048033e1015a7995a169785d7995587995048033e1010400e1f5059596776e947b757c7800a0696854790087535e7900a06376608577687863760120857768005f7900a0635f795680547958807e77686e7e51d28851d15779517e8851d356799d02aa20012060797e5e797eaa7e01877e51cd8852796351cc5579a26952d100876452d101207f75577987916968c4539d6752d158798852d35579a26902aa20525152807e5f797eaa7e01877e52cd8853d100876453d101207f7557798791696854d100876454d101207f75577987916968c4559d6800cf517f77547f758100cc00c6527993a26900cd00c78800cf557f77547f758100cf557f75788b54807e00d28800ce00d1886d6d6d6d686d6d6d6d6d51").unwrap()); +static OFFERING_ENTRY_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("827701209dc0519dc0ce01207f7500ce01207f758800cf517f755f845288c0cf517f7501208401008763c0c878c88876c9529d687551").unwrap()); +static LAUNCHER_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("0xc0009d00c852c88852c9529d00c854c88854c9549d00c855c88855c9559d00ce01207f7551ce78527e8851cf517f755f8401008853ce01207f7555798853cf567f75817600a06953d100876453d101207f7552798791696851d0547aa07651d0557aa09b63537952799f696851cf557f77547f758100a16302aa2001205d797e57797eaa7e01877e556085537956807e0058807e52d058807e0058807e00d28800d154798800d3009d00cd788851cd788851cc52c6a26951d152ce8851d352d09d51d2008852d10088c4549d75675279517e00d18800d3009d766355ca827755ca7853947f77527f758155ca527953945279947f77787f75526085557956807e51cf557f77547f757e0058807e52d058807e0058807e00d28802aa205979827751805a797e01207e60797e52797eaa7e01877e00cd8802aa20012060797e5a797eaa7e01877e52cd8852cc52c6a26952d152ce8852d352d09d52d2008854d10088c4559d6d756754ca827754ca7853947f77527f758154ca527953945279947f77787f7551d07600a06354d152ce8854d3789d54d2008802aa2001200111797e5b797eaa7e01877e54cd8855d10088c4569d6754d10088c4559d6852567956807e51cf557f77547f757e0058807e7858807e0058807e00d28802aa205c79827751805d797e5c79827751807e5c797e5b79827751807e5b797e01207e5a797e547e5f797e01207e60797e01207e0111797e53797eaa7e01877e00cd8852d152ce8852d352d05279949d52cc52c6a26902aa20520052807e5d797eaa7e01877e52cd8852d200886d6d6802aa2056798277518057797e5a797eaa7e01877e51cd88545b797e51d28851cc51c6a26951d153798851d3009d686d6d6d6d6d6d51").unwrap()); +static DISTRIBUTOR_DEPLOY_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("c0009d00c852c88852c9529d00cf517f77567f758100ce01207f7551ce01207f75788851cf517f755f84538851cf517f7551ca51ca82770136940120947f7701207f750000537960840100876451cf517f77567f75817b757c51cf577f77587f758177687800a2697600a26952d352d051d0949d587a810000567901208401008764527900a26951c6765479950400e1f50596537a757c6b7c6c765379947b757c53cc5279a26953d1008854cc5379a26954d10088756753d05379950400e1f505967b757c53d0527994777600a06353d3789d53d153ce886753d10088687800a06354d352799d54d153ce886754d100886868547900a06302aa20012057797e5f797eaa7e01877e51cd8851d1587988565551807e5c797e597956799356807e51d28851d3009d55d351d09d55d152ce8802aa20525152807e60797eaa7e01877e55cd8855d200886751d351d09d51d152ce8802aa20012057797e5e797eaa7e01877e51cd8851d20088687600a06302aa2001205b797e5e797eaa7e01877e53cd886753cd016a88687c00a06302aa2001205b797e5d797eaa7e01877e54cd886754cd016a886800cf577f77547f75817651a06300cf517f7500cf517f77567f757e788c54807e00cf5b7f77587f758153799358807e00cf01137f77587f757e00cf011b7f77587f758155799358807e00d28800ce00d18800c700cd8800d000d39d52cf52d28852ce52d18852c752cd88675500cf517f77567f757e00cf5b7f77587f758153799358807e00cf01137f77587f757e00cf011b7f77587f758155799358807e00d28800d158798800d3009d02aa2001205b797e5e797eaa7e01877e00cd8852d100885457790120840100876475536876ce59798876cf517f755f845488756855557a00a063755668c4789e6376d10088c4788b9d686d6d6d6d6d6d6d7551").unwrap()); +static DISTRIBUTOR_REFUND_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("c0009d00ce01207f7551ce01207f75788851cf517f755f84538851ca51ca82770136940120947f7701207f7551cf517f75760120840100876451cc51c6a26951d100886751d352d09d51d152ce886802aa200120537a7e55797eaa7e01877e51cd8800cf577f77547f75817651a06300cf517f7500cf517f77567f757e788c54807e00cf5b7f77587f757e00cf01137f77587f757e00cf011b7f77587f757e00d28800ce00d18800c700cd88c4529e6352d10088c4539d686755608500cf517f77567f757e00cf5b7f77587f757e00cf01137f77587f757e00cf011b7f77587f757e00d288527900d18800d3009d02aa20012055797e56797eaa7e01877e00cd8852d100885352790120840100876475526876ce54798876cf517f755f845488c4539e6353d10088c4549d6875686d6d7551").unwrap()); +static EXECUTION_FEE_PAYOUT_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("c0ce00ce01207f758800cf517f755f84528800cf577f77547f758151a169c0cf517f7701207f7502aa2001207b7e7b7eaa7e01877e52cd8852ccc0c6a2").unwrap()); +static OFFERING_INITIATOR_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("c0c9009dc0c85a79c8885979c9567a9d5879ce827701209d557900a263c0c85a79c8885979c956799d68c0c800d1788800d2578800d3009d00cd597a88c08bc0c878c88876c9547a9d76ca827778ca7853947f77527f75817bca7b53945279947f777c7f75c05293c0c878c88876c9557a9d76ca827778ca7853947f77527f75817bca7b53945279947f777c7f75c05393c0c878c88876c9567a9d76ca827778ca7853947f77527f75815178529302ff00a063755367785293014ba063755268685379ca537a5394537a947b947f7702aa20525352807e5a797e7b7eaa7e01877e54cd8854cc7cc6a26954d10088c05493c0c878c88876c9567a9d76ca827778ca7853947f77527f75815178529302ff00a063755367785293014ba063755268685379ca537a5394537a947b947f7702aa20525352807e59797e7b7eaa7e01877e55cd8855cc7cc6a26955d100885679d07600a0690100557a7e04000000007e51d28851d15479527e8851d3789d51cd02aa20547aaa7e01877e885153d28853d153798853d3009d53cd02aa20537aaa7e01877e8802aa20525352807e557a7eaa7e01877e52cd8852cc5479c6a26952d1547ace8852d39d52d2008856d100876456d101207f75788791696856cd567f75066a0442434d52879169c4579e6357d100876457d101207f75788791696857cd567f75066a0442434d52879169c4589e6358d100876458d101207f75788791696858cd567f75066a0442434d52879169c4599e6359d100876459d101207f75788791696859cd567f75066a0442434d52879169c45a9e635ad10087645ad101207f7578879169685acd567f75066a0442434d52879169c45b9d686868686d7551").unwrap()); +static OFFERING_INTEGRATED_TIMELOCKED_P2NFTH_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("c0cf527f7701207f75c0cf01227f77815479ce01207f757b88537acf567f75817600a0699f6978ce7bcf7eaa88c0cf517f77517f7581c0c85279c8887cc99c").unwrap()); -let IDO_INITIATOR_CONTRACT = hex::decode("c0ce827701209dc0cf827700a0695579827701209dc0c85779c8885679c99dc0c85779c8885679c99d5479529376ca827778ca7853947f77527f75817bca7b53945279947f777c7f75517f7701207f75c0cec0cf7eaa88547ac852d1827701209d52d300a06902aa2053798277518054797e5379827751807e537a7e01207e7b7e01207e52d17e01207e547a7e587e52d358807e537a7eaa7e01877e56cd8856ccc0c6a26956d1c0ce8856d2c0cf8856d3c0d09d02aa20525652807e7b7eaa7e01877e7657cd8857cc02e803a26957d1008858cd8858cc78c6a26958d178ce8858d278cf8858d37cd09c").expect("Decoding failed"); -let IDO_POSTLAUNCH_CONTRACT = hex::decode("5879009c63c0009dc0c9009e69c0cdc0c788c0ccc0c6a269c0d1c0ce88c0d3c0d09dc0d2c0cf8802aa20520052807e597a7eaa7e01877ec0c851c88851c9579d7651cd8851cc51c6a26951d151ce8851d351d09dc0c852c88852c9589d52cd8852cc52c6a26952d152ce8852d352d09d53d10088c4549d6d6d6d6d51675879519c63c0009dc0c9009dc0cdc0c788c0d1c0ce88c0d3c0d09dc0d2c0cf88c0c852c88852c9529d52cd52c78852cc52c6a26952d152ce8852d352d09d02aa200120c0cec0cf7eaa7e587a7eaa7e01877e00005376c75479876376ce59798763527978d093537a757c6b7c6c6776ce0088686ec6937b757c6776ce008868768bc378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c3789d6868686868686868c0ccc0c6547a93a269c0c851c88851c9519d51cd51c78851cc51c6a26951d0537a937600a06351d159798851d378a26951d200886751d100886853d10088c4549d6d6d6d6d6d6d5167587a529dc0009dc0c9009d5379827701209d5879ce567a885779cf517f755f845588c0c65879cf577f77587f7581a269c0c851c88851c9519d5779cf5f7f77587f75817600a06351d078a2696851d000a06351ce5679886851cf0088c0c852c88852c9529d52ce56798800005a79cf517f75608401008763557981537994765679950400e1f505967652d0a06352d07768765679950500e8764817965d79cf01177f77587f758194760500e8764817955779967802e803a27800a09a6378567a757c6b7c6b7c6b7c6b7c6c6c6c6c765379a16376557a757c6b7c6b7c6b7c6c6c6c675279557a757c6b7c6b7c6b7c6c6c6c68686d6d68c0c651c69352c69352799451d052d0935279945100557900a0557900a09a63537b75775202aa2005746376a914000114807e2b88ac67c0d1c0ce88c25288c0cdc0c788c0c6c0d095c0c6c0cc9490539502e80396c0cc7c94c0d3957ca2687eaa7e01877e00cd8800cc56799d00d15c798800d355799d00d20088096a0653554d4d4f4e14000114807e51cd8851cc009d51d100886802aa2001205c7a7e5d7a7eaa7e01877e78cd8876cc547aa269527900a06376d35379a26976d15a798876d200886776d100886878d10088c47b8b9d6d6d6d6d6d75516868").expect("Decoding failed"); -let IDO_PREINIT_CONTRACT = hex::decode("c0519dc0c800c88800c9529dc0c852c88852c9539dc0c853c88853c9549dc0c854c88854c9559dc0c855c88855c9569dc0c856c88856c9579d5979009e63c0c857c88857c9589d68567981009c5a79009c9b587981009c9b63c0d1008852cd00c78852d1008853cd52c78853d1008854cd53c78854d1008855cd54c78855d1008856cd55c78856d1008857cd56c78857d100885979009e6358cd57c78858cc57c6a26958d157ce8858d357d09d58d257cf886802aa200302010055797eaa7e01877ec101447f77587981009c6301005c79009e63015177685b79009c6300cd53798800d10088760251207e5b797e01207e5a797e52797e7b757c6757585e79009e63587b75775968c0c85279c88878c9009d00cd016a8800d1008876cd5b798876d1c0c88876d3009d76d2010088768bd10088c47852939d52790200207e5d797e01207ec0c87e54797e547a757c6b7c6b7c6c6c6d6875675b79009c635a79009c6300cd52798800d10088030051205a797e01207e59797e787e7767c0c857c88857c9009d53795579935379789454795279a06900cd02a91401205d797e5b797ea97e01877e8800d1c0c88800d3789d00d2008858cd54798858d1c0c88858d352799d58d2008859d10088c45a9d035100205c797e01207e5b797e53797e537a757c6b7c6c6d68675a79009c6300cd52798800d10088035151205a797e01207e59797e787e7767c0c858c88858c9009d00cd016a8800d1008859cd52798859d1c0c88859d3009d59d201ff885ad10088c45b9d03510020c0c87e01207e59797e787e77686868c0cd02aa205279aa7e01877e886d67c0c858c88858c9599d55ca827755ca7853947f77527f758155ca527953945279947f77787f7502a91401205b797e59797ea97e01877e00cd02aa20011753797e54797eaa7e01877e8800d1008800ca827700ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686800ca537953945379945279947f7702aa20030200005d797e52797eaa7e01877e51cd8851d1008852ca827752ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686852ca537953945379945279947f7702aa20030200000111797e707c7f757e01207e01157901ff7eaa7e707c0121937f777eaa7e01877e52cd8852d1008853ca827753ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686853ca537953945379945279947f7702aa20030200000115797e52797eaa7e01877e53cd8853d1008854ca827754ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686854ca537953945379945279947f7702aa20030200000119797e52797eaa7e01877e54cd8854d1008802aa20030200000119797eaa7e01877e55cd8855cc57c6a26955d157ce8855d30116799d55d2008856ca827756ca7853947f77527f758156ca527953945279947f77787f7556cd02aa20012001207901007eaa7e53797eaa7e01877e8856cc58c6a26956d158ce8856d358d09d56d258cf8802aa20525652807e011c797eaa7e01877e57cd8857d157ce8857d357d0011a79949d57d200886d6d6d6d6d6d6d6d6d6d6d75686d6d6d6d6d51").expect("Decoding failed"); - -let STORAGE_CONTRACT = hex::decode("8178c99dc8c0c887").expect("Decoding failed"); -let P2NFTH_CONTRACT = hex::decode("78ce7bcf7eaa87").expect("Decoding failed"); -let CASHTOKENS_STUDIO_PAY2CATEGORY_CONTRACT = hex::decode("51ce8851d0009d6300cdc0c7886851").expect("Decoding failed"); +static IDO_INITIATOR_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("c0ce827701209dc0cf827700a0695579827701209dc0c85779c8885679c99dc0c85779c8885679c99d5479529376ca827778ca7853947f77527f75817bca7b53945279947f777c7f75517f7701207f75c0cec0cf7eaa88547ac852d1827701209d52d300a06902aa2053798277518054797e5379827751807e537a7e01207e7b7e01207e52d17e01207e547a7e587e52d358807e537a7eaa7e01877e56cd8856ccc0c6a26956d1c0ce8856d2c0cf8856d3c0d09d02aa20525652807e7b7eaa7e01877e7657cd8857cc02e803a26957d1008858cd8858cc78c6a26958d178ce8858d278cf8858d37cd09c").unwrap()); +static IDO_POSTLAUNCH_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("5879009c63c0009dc0c9009e69c0cdc0c788c0ccc0c6a269c0d1c0ce88c0d3c0d09dc0d2c0cf8802aa20520052807e597a7eaa7e01877ec0c851c88851c9579d7651cd8851cc51c6a26951d151ce8851d351d09dc0c852c88852c9589d52cd8852cc52c6a26952d152ce8852d352d09d53d10088c4549d6d6d6d6d51675879519c63c0009dc0c9009dc0cdc0c788c0d1c0ce88c0d3c0d09dc0d2c0cf88c0c852c88852c9529d52cd52c78852cc52c6a26952d152ce8852d352d09d02aa200120c0cec0cf7eaa7e587a7eaa7e01877e00005376c75479876376ce59798763527978d093537a757c6b7c6c6776ce0088686ec6937b757c6776ce008868768bc378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c3789d6868686868686868c0ccc0c6547a93a269c0c851c88851c9519d51cd51c78851cc51c6a26951d0537a937600a06351d159798851d378a26951d200886751d100886853d10088c4549d6d6d6d6d6d6d5167587a529dc0009dc0c9009d5379827701209d5879ce567a885779cf517f755f845588c0c65879cf577f77587f7581a269c0c851c88851c9519d5779cf5f7f77587f75817600a06351d078a2696851d000a06351ce5679886851cf0088c0c852c88852c9529d52ce56798800005a79cf517f75608401008763557981537994765679950400e1f505967652d0a06352d07768765679950500e8764817965d79cf01177f77587f758194760500e8764817955779967802e803a27800a09a6378567a757c6b7c6b7c6b7c6b7c6c6c6c6c765379a16376557a757c6b7c6b7c6b7c6c6c6c675279557a757c6b7c6b7c6b7c6c6c6c68686d6d68c0c651c69352c69352799451d052d0935279945100557900a0557900a09a63537b75775202aa2005746376a914000114807e2b88ac67c0d1c0ce88c25288c0cdc0c788c0c6c0d095c0c6c0cc9490539502e80396c0cc7c94c0d3957ca2687eaa7e01877e00cd8800cc56799d00d15c798800d355799d00d20088096a0653554d4d4f4e14000114807e51cd8851cc009d51d100886802aa2001205c7a7e5d7a7eaa7e01877e78cd8876cc547aa269527900a06376d35379a26976d15a798876d200886776d100886878d10088c47b8b9d6d6d6d6d6d75516868").unwrap()); +static IDO_PREINIT_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("c0519dc0c800c88800c9529dc0c852c88852c9539dc0c853c88853c9549dc0c854c88854c9559dc0c855c88855c9569dc0c856c88856c9579d5979009e63c0c857c88857c9589d68567981009c5a79009c9b587981009c9b63c0d1008852cd00c78852d1008853cd52c78853d1008854cd53c78854d1008855cd54c78855d1008856cd55c78856d1008857cd56c78857d100885979009e6358cd57c78858cc57c6a26958d157ce8858d357d09d58d257cf886802aa200302010055797eaa7e01877ec101447f77587981009c6301005c79009e63015177685b79009c6300cd53798800d10088760251207e5b797e01207e5a797e52797e7b757c6757585e79009e63587b75775968c0c85279c88878c9009d00cd016a8800d1008876cd5b798876d1c0c88876d3009d76d2010088768bd10088c47852939d52790200207e5d797e01207ec0c87e54797e547a757c6b7c6b7c6c6c6d6875675b79009c635a79009c6300cd52798800d10088030051205a797e01207e59797e787e7767c0c857c88857c9009d53795579935379789454795279a06900cd02a91401205d797e5b797ea97e01877e8800d1c0c88800d3789d00d2008858cd54798858d1c0c88858d352799d58d2008859d10088c45a9d035100205c797e01207e5b797e53797e537a757c6b7c6c6d68675a79009c6300cd52798800d10088035151205a797e01207e59797e787e7767c0c858c88858c9009d00cd016a8800d1008859cd52798859d1c0c88859d3009d59d201ff885ad10088c45b9d03510020c0c87e01207e59797e787e77686868c0cd02aa205279aa7e01877e886d67c0c858c88858c9599d55ca827755ca7853947f77527f758155ca527953945279947f77787f7502a91401205b797e59797ea97e01877e00cd02aa20011753797e54797eaa7e01877e8800d1008800ca827700ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686800ca537953945379945279947f7702aa20030200005d797e52797eaa7e01877e51cd8851d1008852ca827752ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686852ca537953945379945279947f7702aa20030200000111797e707c7f757e01207e01157901ff7eaa7e707c0121937f777eaa7e01877e52cd8852d1008853ca827753ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686853ca537953945379945279947f7702aa20030200000115797e52797eaa7e01877e53cd8853d1008854ca827754ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686854ca537953945379945279947f7702aa20030200000119797e52797eaa7e01877e54cd8854d1008802aa20030200000119797eaa7e01877e55cd8855cc57c6a26955d157ce8855d30116799d55d2008856ca827756ca7853947f77527f758156ca527953945279947f77787f7556cd02aa20012001207901007eaa7e53797eaa7e01877e8856cc58c6a26956d158ce8856d358d09d56d258cf8802aa20525652807e011c797eaa7e01877e57cd8857d157ce8857d357d0011a79949d57d200886d6d6d6d6d6d6d6d6d6d6d75686d6d6d6d6d51").unwrap()); +static STORAGE_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("8178c99dc8c0c887").unwrap()); +static P2NFTH_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("78ce7bcf7eaa87").unwrap()); +static CASHTOKENS_STUDIO_PAY2CATEGORY_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("51ce8851d0009d6300cdc0c7886851").unwrap()); pub struct IdoPreInitLockParameters { - authguardLockingBytecode: Vec; + authguardLockingBytecode: Vec, permanentLiquidityShareNumerator: BigInt, offeredTokenAmount: BigInt, offeredTokenTotalSupply: BigInt, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct IdoParametersOfferingLaunchConditions { expiresAt: BigInt, deployThreshold: BigInt, immediateDeployThreshold: BigInt, } -#[derive(Serialize, Deserialize)] +#[serde_as] +#[derive(Serialize, Deserialize, Clone)] pub struct IdoParametersOfferingOffer { maxDiscountRateNumerator: BigInt, discountAnnualRateNumerator: BigInt, priceNumerator: BigInt, minOffer: BigInt, - #[serde(with="hex")] + #[serde_as(as = "Option")] xTokenCategory: Option>, } -#[derive(Serialize, Deserialize)] +#[serde_as] +#[derive(Serialize, Deserialize, Clone)] pub struct IdoParametersOffering { platformFeeNumerator: BigInt, - #[serde(with="hex")] + #[serde_as(as = "serde_with::hex::Hex")] platformFeeNFTH: Vec, - #[serde(with="hex")] + #[serde_as(as = "serde_with::hex::Hex")] delphiCategory: Vec, launchConditions: IdoParametersOfferingLaunchConditions, offer: IdoParametersOfferingOffer, executionFee: BigInt, } -#[derive(Serialize, Deserialize)] +#[serde_as] +#[derive(Serialize, Deserialize, Clone)] pub struct IdoPreInitParameters { - #[serde(with="hex")] + #[serde_as(as = "serde_with::hex::Hex")] authguardLockingBytecode: Vec, authguardNftOutputIndex: BigInt, permanentLiquidityShareNumerator: BigInt, @@ -118,39 +141,40 @@ pub struct IdoPreInitParameters { offering: IdoParametersOffering, } -#[derive(Serialize, Deserialize)] +#[serde_as] +#[derive(Serialize, Deserialize, Clone)] pub struct IdoActiveParameters { - #[serde(with="hex")] + #[serde_as(as = "serde_with::hex::Hex")] collectorNFTH: Vec, permanentLiquidityShareNumerator: BigInt, offeredTokenAmount: BigInt, offering: IdoParametersOffering, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] #[serde(tag = "type")] pub enum IdoParameters { - PreInit(IdoPreInitParameters); - Active(IdoActiveParameters); + PreInit(IdoPreInitParameters), + Active(IdoActiveParameters), } -#[derive(Serialize, Deserialize)] +#[serde_as] +#[derive(Serialize, Deserialize, Clone)] pub struct IdoPreInitState { - #[serde(with="hex")] + #[serde_as(as = "serde_with::hex::Hex")] authguardCategory: Vec, - #[serde(with="hex")] + #[serde_as(as = "serde_with::hex::Hex")] idoCategory: Vec, nextTxSetterFlag: BigInt, oTokenGenerated: BigInt, - offeredTokenIsInSupply: bool, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct IdoActiveState { counter: BigInt, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct IdoDistributingState { isRefund: bool, timestamp: BigInt, @@ -161,231 +185,266 @@ pub struct IdoDistributingState { platformEarnedAmount: BigInt, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct IdoPostLaunchState { isRefund: bool, - timestamp: bool, + timestamp: BigInt, earnedAmount: BigInt, refundAmount: BigInt, discountAmount: BigInt, platformEarnedAmount: BigInt, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct IdoPermanentPoolV0 { tokenAmount: BigInt, satoshiAmount: BigInt, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct IdoDistributedState { - permanentPool: IdoPermanentPoolV0, + permanentPool: Option, refundAmount: BigInt, discountAmount: BigInt, collectorEarnedAmount: BigInt, platformEarnedAmount: BigInt, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] #[serde(tag = "type")] pub enum IdoState { - PreInit(IdoPreInitState); - Active(IdoActiveState); - Distributing(IdoDistributingState); - PostLaunch(IdoPostLaunchState); - Distributed(IdoDistributedState); + PreInit(IdoPreInitState), + Active(IdoActiveState), + Distributing(IdoDistributingState), + PostLaunch(IdoPostLaunchState), + Distributed(IdoDistributedState), } +/*UNUSED fn script_concat(script: Script, v: &[u8]) -> Script { - Script([script.as_bytes(), v].concat().into_boxed_slice()) + let mut bytes = script.to_bytes(); + bytes.extend_from_slice(v); + Script::from(bytes) +} +*/ +fn build_p2nfth_script(nfthash: &[u8]) -> Script { + let mut bytes = Builder::new() + .push_slice(nfthash) + .into_script() + .to_bytes(); + bytes.extend_from_slice(&P2NFTH_CONTRACT); + Script::from(bytes) } -fn build_p2nfth_script (nfthash: &[u8]) -> Script { - let builder = Builder::new() - .push_slice(nfthash); - Script([builder.into_script().0, P2NFTH_CONTRACT].concat()); +fn build_storage_script(governingOutpointIndex: u32) -> Script { + let encoded = encode_padded_vm_number(&BigInt::from(governingOutpointIndex), 2).unwrap(); + let mut bytes = Builder::new() + .push_slice(&encoded) + .into_script() + .to_bytes(); + bytes.extend_from_slice(&STORAGE_CONTRACT); + Script::from(bytes) } -fn build_storage_script (governingOutpointIndex: u32) -> Script { - let builder = Builder::new() - .push_slice(encode_padded_vm_number(BigInt::from(governingOutpointIndex), 2)); - Script([builder.into_script().0, STORAGE_CONTRACT].concat()); -} -fn build_storage_script_with_data (governingOutpointIndex: u32, data: &[u8]) -> Script { - let builder0 = Builder::new() - .push_slice(bigint_to_vm_number(BigInt::from(governingOutpointIndex))); - let builder1 = Builder::new() +fn build_storage_script_with_data(governingOutpointIndex: u32, data: &[u8]) -> Script { + let mut bytes = Builder::new() + .push_slice(&bigint_to_vm_number(&BigInt::from(governingOutpointIndex))) + .into_script() + .to_bytes(); + bytes.extend_from_slice(&STORAGE_CONTRACT); + let suffix = Builder::new() .push_slice(data) - .push_opcode(opcodes::all::OP_DROP); - return Script([builder0.0, STORAGE_CONTRACT, builder1.0].concat().into_boxed_slice()); + .push_opcode(opcodes::all::OP_DROP) + .into_script() + .to_bytes(); + bytes.extend_from_slice(&suffix); + Script::from(bytes) } -fn extract_data_from_unlocking_bytecode_of_storage_with_data (unlocking_bytecode: Script) -> Result> { - let instructions = unlocking_bytecode.instructions(); - if instructions.count() != 2 { - return Err("not a storage unlocking bytecode, should have only two push opcodes"); + +fn extract_data_from_unlocking_bytecode_of_storage_with_data(unlocking_bytecode: Script) -> Result> { + let instructions: Vec<_> = unlocking_bytecode.instructions().collect(); + if instructions.len() != 2 { + return Err(anyhow::anyhow!("not a storage unlocking bytecode, should have only two push opcodes")); } - match instructions[1] { - Ok(Instruction::PushBytes(redeem_bytecode)) => { - let redeem_inst_list = Script(redeem_bytecode).instructions(); - match redeem_inst_list[redeem_inst_list.count() - 2] { - Ok(Instruction::PushBytes(data)) => Ok(data), - _ => return Err("storage with data should have a push opcode at instructions[count - 2]"), + match &instructions[1] { + Ok(Instruction::PushBytes(redeem_bytecode)) => { + let inner_script = Script::from(redeem_bytecode.to_vec()); + let redeem_inst_list: Vec<_> = inner_script.instructions().collect(); + let n = redeem_inst_list.len(); + if n < 2 { + return Err(anyhow::anyhow!("storage with data should have at least two instructions")); + } + match &redeem_inst_list[n - 2] { + Ok(Instruction::PushBytes(data)) => Ok(data.to_vec()), + _ => Err(anyhow::anyhow!("storage with data should have a push opcode at instructions[count - 2]")), } }, - _ => return Err("invalid unlocking bytecode, p2sh redeem script push opcode is expected"), + _ => Err(anyhow::anyhow!("invalid unlocking bytecode, p2sh redeem script push opcode is expected")), } } -fn is_script_contain_ido_signature (unlocking_bytecode: Script) -> bool { - let instructions = unlocking_bytecode.instructions(); - match instructions[instructions.count() - 1] { - Ok(Instruction::PushBytes(redeem_bytecode)) => { - if redeem_bytecode[0..IDO_SIGNATURE.len()] == IDO_SIGNATURE { - true - } else { - false - } - }, - _ => false, +fn has_script_ido_signature(unlocking_bytecode: &Script) -> bool { + let instructions: Vec<_> = unlocking_bytecode.instructions().collect(); + match instructions.last() { + Some(Ok(Instruction::PushBytes(redeem_bytecode))) => { + redeem_bytecode.len() >= IDO_SIGNATURE.len() && + &redeem_bytecode[0..IDO_SIGNATURE.len()] == IDO_SIGNATURE + }, + _ => false, } } -fn build_offering_bytecode(maxDiscountRate: BigInt, discountAnnualRate: BigInt, price: BigInt, minOffer: BigInt, xTokenCategory: &[u8]) -> Vec { - let builder = Builder::from( - Builder::new() - .push_slice(STORAGE_CONTRACT) - .push_slice(OFFERING_ENTRY_CONTRACT) - .into_script().0.extend( - [ - bigint_to_push_opcode(maxDiscountRate), - bigint_to_push_opcode(discountAnnualRate), - bigint_to_push_opcode(price), - bigint_to_push_opcode(minOffer), - ].concat() - ) - ) - .push_slice(xTokenCategory); - [IDO_SIGNATURE, builder.0, OFFERING_CONTRACT].concat() +fn build_offering_bytecode(maxDiscountRate: &BigInt, discountAnnualRate: &BigInt, price: &BigInt, minOffer: &BigInt, xTokenCategory: &[u8]) -> Vec { + let mut input_bytecode = Builder::new() + .push_slice(&STORAGE_CONTRACT) + .push_slice(&OFFERING_ENTRY_CONTRACT) + .into_script() + .to_bytes(); + input_bytecode.extend_from_slice(&bigint_to_push_opcode(maxDiscountRate)); + input_bytecode.extend_from_slice(&bigint_to_push_opcode(discountAnnualRate)); + input_bytecode.extend_from_slice(&bigint_to_push_opcode(price)); + input_bytecode.extend_from_slice(&bigint_to_push_opcode(minOffer)); + input_bytecode.extend_from_slice(&Builder::new() + .push_slice(xTokenCategory) + .into_script() + .to_bytes() + ); + let mut bytecode = IDO_SIGNATURE.to_vec(); + bytecode.extend_from_slice(&input_bytecode); + bytecode.extend_from_slice(&OFFERING_CONTRACT); + bytecode } -fn build_launcher_bytecode(collectorNFTH: &[u8], platformFeeNFTH: &[u8], platformFee: BigInt, delphiCategory: &[u8], expiresAt: BigInt, deployThreshold: BigInt, immediateDeployThreshold: BigInt) -> Vec { - let input_bytecode = Builder::new() + +fn build_launcher_bytecode(collectorNFTH: &[u8], platformFeeNFTH: &[u8], platformFee: &BigInt, delphiCategory: &[u8], expiresAt: &BigInt, deployThreshold: &BigInt, immediateDeployThreshold: &BigInt) -> Vec { + let mut input_bytecode = Builder::new() .push_slice(collectorNFTH) .push_slice(platformFeeNFTH) - .push_slice(encode_padded_vm_number(platformFee, 4)) - .push_slice(EXECUTION_FEE_PAYOUT_CONTRACT) - .push_slice(STORAGE_CONTRACT) - .push_slice(OFFERING_INTEGRATED_TIMELOCKED_P2NFTH_CONTRACT) - .push_slice(P2NFTH_CONTRACT) + .push_slice(&encode_padded_vm_number(&platformFee, 4).unwrap()) + .push_slice(&EXECUTION_FEE_PAYOUT_CONTRACT) + .push_slice(&STORAGE_CONTRACT) + .push_slice(&OFFERING_INTEGRATED_TIMELOCKED_P2NFTH_CONTRACT) + .push_slice(&P2NFTH_CONTRACT) .push_slice(delphiCategory) - .0.extend( - [ - bigint_to_push_opcode(expiresAt), - bigint_to_push_opcode(deployThreshold), - bigint_to_push_opcode(immediateDeployThreshold), - ].concat() - ); - [IDO_SIGNATURE, input_bytecode, LAUNCHER_CONTRACT].concat(); -} -fn build_partial_offering_initiator_bytecode(extensionOutpointIndex: BigInt, tokenStorageOutpointIndex: BigInt, distRefundOutpointIndex: BigInt, distDeployOutpointIndex: BigInt, launcherBytecodeOutpointIndex: BigInt, offeringBytecodeOutpointIndex: BigInt, executionFee: BigInt) -> Vec { - let input_bytecode = Builder::new() - .push_slice(STORAGE_CONTRACT) - .0.extend( - [ - bigint_to_push_opcode(extensionOutpointIndex), - bigint_to_push_opcode(tokenStorageOutpointIndex), - bigint_to_push_opcode(distRefundOutpointIndex), - bigint_to_push_opcode(distDeployOutpointIndex), - bigint_to_push_opcode(launcherBytecodeOutpointIndex), - bigint_to_push_opcode(offeringBytecodeOutpointIndex), - bigint_to_push_opcode(executionFee), - ].concat() - ); - [input_bytecode, OFFERING_INITIATOR_CONTRACT].concat() + .into_script() + .to_bytes(); + input_bytecode.extend_from_slice(&bigint_to_push_opcode(expiresAt)); + input_bytecode.extend_from_slice(&bigint_to_push_opcode(deployThreshold)); + input_bytecode.extend_from_slice(&bigint_to_push_opcode(immediateDeployThreshold)); + let mut bytecode = IDO_SIGNATURE.to_vec(); + bytecode.extend_from_slice(&input_bytecode); + bytecode.extend_from_slice(&LAUNCHER_CONTRACT); + bytecode } -fn build_partial_postlaunch_bytecode(permanentLiquidityShare: BigInt, price: BigInt) -> Vec { - [ - bigint_to_push_opcode(permanentLiquidityShare), - bigint_to_push_opcode(price), - IDO_POSTLAUNCH_CONTRACT, - ].concat() +fn build_partial_offering_initiator_bytecode(extensionOutpointIndex: &BigInt, tokenStorageOutpointIndex: &BigInt, distRefundOutpointIndex: &BigInt, distDeployOutpointIndex: &BigInt, launcherBytecodeOutpointIndex: &BigInt, offeringBytecodeOutpointIndex: &BigInt, executionFee: &BigInt) -> Vec { + let mut bytecode = Builder::new() + .push_slice(&STORAGE_CONTRACT) + .into_script() + .to_bytes(); + bytecode.extend_from_slice(&bigint_to_push_opcode(extensionOutpointIndex)); + bytecode.extend_from_slice(&bigint_to_push_opcode(tokenStorageOutpointIndex)); + bytecode.extend_from_slice(&bigint_to_push_opcode(distRefundOutpointIndex)); + bytecode.extend_from_slice(&bigint_to_push_opcode(distDeployOutpointIndex)); + bytecode.extend_from_slice(&bigint_to_push_opcode(launcherBytecodeOutpointIndex)); + bytecode.extend_from_slice(&bigint_to_push_opcode(offeringBytecodeOutpointIndex)); + bytecode.extend_from_slice(&bigint_to_push_opcode(executionFee)); + bytecode.extend_from_slice(&OFFERING_INITIATOR_CONTRACT); + bytecode } -fn build_partial_ido_initaitor_bytecode(postLaunchPartialBytecode: &[u8], permanentPoolOTokenReserveOutpointIndex: BigInt, offeringInitiatorOutpointIndex: BigInt) -> Vec { - let input_bytecode = Builder::new() +fn build_partial_postlaunch_bytecode(permanentLiquidityShare: &BigInt, price: &BigInt) -> Vec { + let mut bytecode = Vec::new(); + bytecode.extend_from_slice(&bigint_to_push_opcode(permanentLiquidityShare)); + bytecode.extend_from_slice(&bigint_to_push_opcode(price)); + bytecode.extend_from_slice(&IDO_POSTLAUNCH_CONTRACT); + bytecode +} + +fn build_partial_ido_initaitor_bytecode(postLaunchPartialBytecode: &[u8], permanentPoolOTokenReserveOutpointIndex: &BigInt, offeringInitiatorOutpointIndex: &BigInt) -> Vec { + let mut bytecode = Builder::new() .push_slice(postLaunchPartialBytecode) - .push_slice(STORAGE_CONTRACT) - .push_slice(P2NFTH_CONTRACT) - .0.extend( - [ - bigint_to_push_opcode(permanentPoolOTokenReserveOutpointIndex), - bigint_to_push_opcode(offeringInitiatorOutpointIndex), - ].concat() - ); - [input_bytecode, IDO_INITIATOR_CONTRACT].concat() + .push_slice(&STORAGE_CONTRACT) + .push_slice(&P2NFTH_CONTRACT) + .into_script() + .to_bytes(); + bytecode.extend_from_slice(&bigint_to_push_opcode(permanentPoolOTokenReserveOutpointIndex)); + bytecode.extend_from_slice(&bigint_to_push_opcode(offeringInitiatorOutpointIndex)); + bytecode.extend_from_slice(&IDO_INITIATOR_CONTRACT); + bytecode } -fn build_preinit_bytecode(parameters: IdoPreInitLockParameters, state: IdoPreInitState) -> Vec { - let input_bytecode_middle = Builder::new() - // reverse - .push_slice(state.idoCategory.iter().copied().rev().collect()) - // reverse - .push_slice(state.authguardCategory.iter().copied().rev().collect()) - .push_slice(parameters.authguardLockingBytecode) - .push_slice(CASHTOKENS_STUDIO_PAY2CATEGORY_CONTRACT) - .push_slice(STORAGE_CONTRACT); - [ - IDO_SIGNATURE, - bigint_to_push_opcode(state.oTokenGenerated), - bigint_to_push_opcode(state.nextTxSetterFlag), - input_bytecode_middle.into_script().0, - bigint_to_push_opcode(parameters.permanentLiquidityShareNumerator), - bigint_to_push_opcode(parameters.offeredTokenAmount), - bigint_to_push_opcode(parameters.offeredTokenTotalSupply), - IDO_PREINIT_CONTRACT, - ].concat() + +fn build_preinit_bytecode(parameters: &IdoPreInitLockParameters, state: &IdoPreInitState) -> Vec { + let rev_ido: Vec = state.idoCategory.iter().copied().rev().collect(); + let rev_authguard: Vec = state.authguardCategory.iter().copied().rev().collect(); + let middle = Builder::new() + .push_slice(&rev_ido) + .push_slice(&rev_authguard) + .push_slice(¶meters.authguardLockingBytecode) + .push_slice(&CASHTOKENS_STUDIO_PAY2CATEGORY_CONTRACT) + .push_slice(&STORAGE_CONTRACT) + .into_script() + .to_bytes(); + let mut bytecode = IDO_SIGNATURE.to_vec(); + bytecode.extend_from_slice(&bigint_to_push_opcode(&state.oTokenGenerated)); + bytecode.extend_from_slice(&bigint_to_push_opcode(&state.nextTxSetterFlag)); + bytecode.extend_from_slice(&middle); + bytecode.extend_from_slice(&bigint_to_push_opcode(¶meters.permanentLiquidityShareNumerator)); + bytecode.extend_from_slice(&bigint_to_push_opcode(¶meters.offeredTokenAmount)); + bytecode.extend_from_slice(&bigint_to_push_opcode(¶meters.offeredTokenTotalSupply)); + bytecode.extend_from_slice(&IDO_PREINIT_CONTRACT); + bytecode } fn build_p2sh32_script(bytecode: &[u8]) -> Script { Builder::new() .push_opcode(opcodes::all::OP_HASH256) - .push_slice(sha256d::hash(bytecode).as_byte_array()) + .push_slice(sha256d::Hash::hash(bytecode).as_inner()) .push_opcode(opcodes::all::OP_EQUAL) .into_script() } - +/* +//UNUSED fn build_p2sh20_script(bytecode: &[u8]) -> Script { Builder::new() .push_opcode(opcodes::all::OP_HASH160) - .push_slice(hash160::hash(bytecode).as_byte_array()) + .push_slice(hash160::Hash::hash(bytecode).as_inner()) .push_opcode(opcodes::all::OP_EQUAL) .into_script() } +*/ pub fn bigint_to_push_opcode(n: &BigInt) -> Vec { - if(n > BigInt::from(0) && n <= BigInt::from(16)) { - vec[ 0x50 + n.to_i8() ]; - } else if n == BigInt::ZERO { - vec[ 0x00 ]; - } else if n == BigInt::from(-1) { - vec[ 0x4f ]; + if *n > BigInt::from(0i64) && *n <= BigInt::from(16i64) { + vec![0x50_u8 + n.to_u8().unwrap()] + } else if *n == BigInt::from(0i64) { + vec![0x00_u8] + } else if *n == BigInt::from(-1i64) { + vec![0x4f_u8] } else { let bytes = bigint_to_vm_number(n); if bytes.len() < 76 { - vec[ bytes.len() ].extend(bytes) + let mut r = vec![bytes.len() as u8]; + r.extend(bytes); + r } else if bytes.len() < 255 { - vec[ 0x4c, bytes.len() ].extend(bytes) + let mut r = vec![0x4c_u8, bytes.len() as u8]; + r.extend(bytes); + r } else { - vec[ 0x4d, (bytes.len() as u16).to_le_bytes() ].extend(bytes) + let mut r = vec![0x4d_u8]; + r.extend_from_slice(&(bytes.len() as u16).to_le_bytes()); + r.extend(bytes); + r } } } -pub fn encode_padded_vm_number(v: BigInt, length: usize) -> Result> { +pub fn encode_padded_vm_number(v: &BigInt, length: usize) -> Result> { let encoded = bigint_to_vm_number(v); if encoded.len() > length { - Err("The value does not fit in the target length") + Err(anyhow::anyhow!("The value does not fit in the target length")) } else { - Ok(pad_minimally_encoded_vm_number(encoded, length)) + Ok(pad_minimally_encoded_vm_number(&encoded, length)) } } @@ -400,14 +459,15 @@ pub fn vm_number_to_bigint(bytes: &[u8]) -> BigInt { // Copy bytes so we can modify the sign bit if necessary let mut data = bytes.to_vec(); - let last_byte = data[data.len() - 1]; - + let last_idx = data.len() - 1; + let last_byte = data[last_idx]; + // Check the sign bit (the high bit of the last byte) let is_negative = (last_byte & 0x80) != 0; if is_negative { // Mask out the sign bit for magnitude calculation - data[data.len() - 1] &= 0x7F; + data[last_idx] &= 0x7F; } // Convert little-endian bytes to BigInt @@ -456,21 +516,21 @@ pub fn pad_minimally_encoded_vm_number(bin: &[u8], length: usize) -> Vec { } let mut padded = bin.to_vec(); - + if let Some(&last_byte) = bin.last() { // Check if the current last byte has the sign bit set (0x80) if (last_byte & 0x80) != 0 { // Remove the sign bit from the current byte let last_idx = padded.len() - 1; padded[last_idx] &= 0x7f; - + // Pad with zeros until the second-to-last byte padded.resize(length - 1, 0x00); - + // Re-apply the sign bit to the new last byte padded.push(0x80); } else { - // If the number is positive and the sign bit isn't used, + // If the number is positive and the sign bit isn't used, // just pad with zeros. padded.resize(length, 0x00); } @@ -485,7 +545,7 @@ pub fn pad_minimally_encoded_vm_number(bin: &[u8], length: usize) -> Vec { pub async fn prepare_tables(pool: &SqlitePool) { sqlx::query( "CREATE TABLE ido ( - id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, preinit_txid BLOB NOT NULL UNIQUE, init_txid BLOB NULL, launch_txid BLOB NULL, @@ -537,7 +597,7 @@ pub async fn prepare_tables(pool: &SqlitePool) { prev_id INTEGER NULL, ido_id INTEGER NOT NULL REFERENCES ido(id), txid BLOB NOT NULL UNIQUE, - tx BLOB NOT NULL, + tx BLOB NOT NULL )", ) .execute(pool) @@ -562,8 +622,9 @@ pub async fn prepare_tables(pool: &SqlitePool) { .expect("failed to create ido_txchain_tracker_map table"); } +/*NOTUSED pub struct IdoEntryDBRecord { - ido_id: u64, + ido_id: i64, txid: Vec, owner_nfthash: Vec, commitment: Vec, @@ -573,21 +634,23 @@ pub struct IdoEntryDBRecord { discount: u64, distributed: bool, } +*/ #[derive(Debug, Clone, serde::Serialize)] pub struct IdoDBRecord { - id: u64; - preinit_txid: Vec , + id: i64, + preinit_txid: Vec, init_txid: Option>, launch_txid: Option>, offering_token_id: Option>, + offered_token_id: Option>, status: String, parameters: Vec, state: Vec, - txchain_entrypoint: Option, - txchain_head: Option, + txchain_entrypoint: Option, + txchain_head: Option, is_valid: bool, -}; +} impl IdoDBRecord { fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result { Ok(Self { @@ -596,24 +659,25 @@ impl IdoDBRecord { init_txid: row.get(2), launch_txid: row.get(3), offering_token_id: row.get(4), - status: row.get(5), - parameters: row.get(6), - state: row.get(7), - txchain_entrypoint: row.get(8), - txchain_head: row.get(9), - is_valid: row.get(10), + offered_token_id: row.get(5), + status: row.get(6), + parameters: row.get(7), + state: row.get(8), + txchain_entrypoint: row.get(9), + txchain_head: row.get(10), + is_valid: row.get(11), }) } } #[derive(Debug, Clone, serde::Serialize)] pub struct IdoTxChainDBRecord { - id: u64, - prev_id: Option, - ido_id: u64, + id: i64, + prev_id: Option, + ido_id: i64, txid: Vec, tx: Vec, -}; +} impl IdoTxChainDBRecord { fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result { Ok(Self { @@ -628,7 +692,7 @@ impl IdoTxChainDBRecord { async fn get_ido_txchain_list( pool: &SqlitePool, - ido_id: u64, + ido_id: i64, ) -> Result> { sqlx::query( "SELECT id, prev_id, ido_id, txid, tx @@ -636,19 +700,20 @@ async fn get_ido_txchain_list( ) .bind(ido_id) .fetch_all(pool) - .await + .await? .iter() .map(|a| IdoTxChainDBRecord::from_row(a)) + .collect() } async fn txchain_lookup_next( pool: &SqlitePool, txid: &Txid, - index: i32, + index: u32, ) -> Result> { - let row = sqlx::query_as( + let row = sqlx::query( "SELECT t1.id, t1.prev_id, t1.ido_id, t1.txid, t1.tx - FROM ido_txchain_tracker_map t0 LEFT JOIN ido_ido_txchain t1 ON t0.txchain_id = t1.id WHERE t0.txid = ? AND t0.next_output_index = ?", + FROM ido_txchain_tracker_map t0 LEFT JOIN ido_txchain t1 ON t0.txchain_id = t1.id WHERE t0.txid = ? AND t0.next_output_index = ?", ) .bind(txid.to_blob()) .bind(index) @@ -661,11 +726,29 @@ async fn txchain_lookup_next( } } +async fn get_txchain_item_by_txid( + pool: &SqlitePool, + txid: &Txid, +) -> Result> { + let row = sqlx::query( + "SELECT id, prev_id, ido_id, txid, tx + FROM ido_txchain WHERE txid = ?", + ) + .bind(txid.to_blob()) + .fetch_optional(pool) + .await?; + if let Some(row) = row { + Ok(Some(IdoTxChainDBRecord::from_row(&row)?)) + } else { + Ok(None) + } +} + async fn ido_lookup( pool: &SqlitePool, - ido_id: u64, + ido_id: i64, ) -> Result> { - let row = sqlx::query_as( + let row = sqlx::query( "SELECT id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, status, parameters, state, txchain_entrypoint, txchain_head, is_valid FROM ido WHERE id = ?", @@ -680,24 +763,22 @@ async fn ido_lookup( } } -let TRACKER_MAX_BLOCK_DEPTH = 1000; +const TRACKER_MAX_BLOCK_DEPTH: i32 = 1000; async fn txchain_trim_tracker( pool: &SqlitePool, - current_block_height: i32, + current_block_height: i64, ) -> Result<()> { - match sqlx::query( - "DELETE FROM ido_txchain_tracker_map WHERE height < ? and height >= 0", - ) - .bind(current_block_height - TRACKER_MAX_BLOCK_DEPTH) - .execute(pool) - .await { - Ok(r) => Ok(()), - Err(e) => Err(e), - } + sqlx::query( + "DELETE FROM ido_txchain_tracker_map WHERE height < ? and height >= 0", + ) + .bind(current_block_height - TRACKER_MAX_BLOCK_DEPTH as i64) + .execute(pool) + .await?; + Ok(()) } -let IDO_PREINIT_ANNOUNCEMENT_SIGNATURE: &[u8; 15] = &[ +const IDO_PREINIT_ANNOUNCEMENT_SIGNATURE: &[u8; 15] = &[ 0x6a, 0x4c, 0xba, 0x43, 0x61, 0x75, 0x6c, 0x64, 0x72, 0x6f, 0x6e, 0x49, 0x64, 0x6f, 0x30, ]; @@ -705,8 +786,12 @@ let IDO_PREINIT_ANNOUNCEMENT_SIGNATURE: &[u8; 15] = &[ fn is_preinit_broadcast( tx: &Transaction, ) -> bool { - let initialLBC = tx.output.get(0).unwrap().script_pubkey.into_bytes()[0..15]; - return initialLBC == IDO_PREINIT_ANNOUNCEMENT_SIGNATURE; + if let Some(out) = tx.output.get(0) { + let bytes = out.script_pubkey.as_bytes(); + bytes.len() >= 15 && &bytes[0..15] == IDO_PREINIT_ANNOUNCEMENT_SIGNATURE + } else { + false + } } struct IdoContext { @@ -721,277 +806,283 @@ struct IdoContext { offering_token_id: Option>, } -async fn create_ido_context_from_preinit(tx) -> Result { - let offered_token_is_in_supply = false; - let is_valid_ido = true; +async fn create_ido_context_from_preinit(network: Option, tx: &Transaction) -> Result { + let platform_fee_nfth = match network { + Some(Network::Chipnet) => CHIPNET_PLATFORM_FEE_NFTH.clone(), + Some(Network::Bitcoin) => MAINNET_PLATFORM_FEE_NFTH.clone(), + _ => MAINNET_PLATFORM_FEE_NFTH.clone(), + }; + let mut offered_token_is_in_supply = false; + let mut is_valid_ido = true; // verify the ido is created correctly let preinit_parameters: IdoPreInitParameters; // preinit announcement let annOut = tx.output.get(0); // preinit params if annOut.is_some() { - let instructions = annOut.unwrap().script_pubkey.instructions(); - if instructions.count() != 3 { - // valid = false - return; + let instructions: Vec<_> = annOut.unwrap().script_pubkey.instructions().collect(); + if instructions.len() != 3 { + return Err(anyhow::anyhow!("should have 3 opcodes")); } // authguard locking bytecode let authguardLockingBytecode; - match instructions[2] { + match &instructions[2] { Ok(Instruction::PushBytes(data)) => { - authguardLockingBytecode = data + authguardLockingBytecode = data.to_vec(); } _ => { - // valid = true - return Err("invalid announcement (3)"); + return Err(anyhow::anyhow!("invalid announcement (3)")); } } // params main - match instructions[1] { + match &instructions[1] { Ok(Instruction::PushBytes(data)) => { preinit_parameters = IdoPreInitParameters { authguardLockingBytecode: authguardLockingBytecode, offering: IdoParametersOffering { platformFeeNFTH: data[10..42].to_vec(), - platformFeeNumerator: vm_number_to_bigint(data[42..46]), - delphiCategory: data[46..78].to_vec() + platformFeeNumerator: vm_number_to_bigint(&data[42..46]), + delphiCategory: data[46..78].to_vec(), launchConditions: IdoParametersOfferingLaunchConditions { - expiresAt: vm_number_to_bigint(data[78..84]), - deployThreshold: vm_number_to_bigint(data[84..92]), - immediateDeployThreshold: vm_number_to_bigint(data[92..100]), + expiresAt: vm_number_to_bigint(&data[78..84]), + deployThreshold: vm_number_to_bigint(&data[84..92]), + immediateDeployThreshold: vm_number_to_bigint(&data[92..100]), }, offer: IdoParametersOfferingOffer { - maxDiscountRateNumerator: vm_number_to_bigint(data[100..104]), - discountAnnualRateNumerator: vm_number_to_bigint(data[104..108]), - priceNumerator: vm_number_to_bigint(data[108..124]), - minOffer: vm_number_to_bigint(data[124..128]), - xTokenCategory: if vm_number_to_bigint(data[128..160]) == BigInt::ZERO { + maxDiscountRateNumerator: vm_number_to_bigint(&data[100..104]), + discountAnnualRateNumerator: vm_number_to_bigint(&data[104..108]), + priceNumerator: vm_number_to_bigint(&data[108..124]), + minOffer: vm_number_to_bigint(&data[124..128]), + xTokenCategory: if vm_number_to_bigint(&data[128..160]) == BigInt::from(0) { None } else { - data[128..160].to_vec() + Some(data[128..160].to_vec()) }, }, - executionFee: vm_number_to_bigint(data[160..164]), + executionFee: vm_number_to_bigint(&data[160..164]), }, - permanentLiquidityShareNumerator: vm_number_to_bigint(data[164..168]), - offeredTokenTotalSupply: vm_number_to_bigint(data[168..176]), - offeredTokenAmount: vm_number_to_bigint(data[176..184]), - authguardNftOutputIndex: vm_number_to_bigint(data[184..185]), + permanentLiquidityShareNumerator: vm_number_to_bigint(&data[164..168]), + offeredTokenTotalSupply: vm_number_to_bigint(&data[168..176]), + offeredTokenAmount: vm_number_to_bigint(&data[176..184]), + authguardNftOutputIndex: vm_number_to_bigint(&data[184..185]), }; // enforce limitation (xToken == NATIVE_BCH) if preinit_parameters.offering.offer.xTokenCategory.is_some() { - return Err("non-null xTokenCategory is not supported"); + return Err(anyhow::anyhow!("non-null xTokenCategory is not supported")); } - if(preinit_parameters.offeredTokenTotalSupply < BigInt::ZERO) { + if preinit_parameters.offeredTokenTotalSupply < BigInt::from(0) { offered_token_is_in_supply = true; } else { - let permanentLiquidityOTokenReserve: BigInt = preinit_parameters.offeredTokenAmount * parameters.permanentLiquidityShareNumerator / PERMANENT_LIQUIDITY_SHARE_DENOMINATOR; - if(preinit_parameters.offeredTokenTotalSupply <= preinit_parameters.offeredTokenAmount + permanentLiquidityOTokenReserve) { - info("ido parse, not a valid ido, preinit_parameters.offeredTokenTotalSupply <= preinit_parameters.offeredTokenAmount + permanentLiquidityOTokenReserve"); + let permanentLiquidityOTokenReserve: BigInt = &preinit_parameters.offeredTokenAmount * &preinit_parameters.permanentLiquidityShareNumerator / &PERMANENT_LIQUIDITY_SHARE_DENOMINATOR.clone(); + if preinit_parameters.offeredTokenTotalSupply <= &preinit_parameters.offeredTokenAmount + &permanentLiquidityOTokenReserve { + info!("ido parse, not a valid ido, preinit_parameters.offeredTokenTotalSupply <= preinit_parameters.offeredTokenAmount + permanentLiquidityOTokenReserve"); is_valid_ido = false; } } - if(preinit_parameters.permanentLiquidityShareNumerator < MIN_PLP_SHARE || - preinit_parameters.permanentLiquidityShareNumerator > MAX_PLP_SHARE || - preinit_parameters.offering.offer.discountAnnualRateNumerator < MIN_PLP_ANNUAL_DISCOUNT || - preinit_parameters.offering.offer.discountAnnualRateNumerator > MAX_PLP_ANNUAL_DISCOUNT || - preinit_parameters.offering.offer.maxDiscountRateNumerator != + if preinit_parameters.permanentLiquidityShareNumerator < *MIN_PLP_SHARE || + preinit_parameters.permanentLiquidityShareNumerator > *MAX_PLP_SHARE || + preinit_parameters.offering.offer.discountAnnualRateNumerator < *MIN_PLP_ANNUAL_DISCOUNT || + preinit_parameters.offering.offer.discountAnnualRateNumerator > *MAX_PLP_ANNUAL_DISCOUNT || + preinit_parameters.offering.offer.maxDiscountRateNumerator != preinit_parameters.permanentLiquidityShareNumerator || - preinit_parameters.offeredTokenAmount <= BigInt::ZERO || + preinit_parameters.offeredTokenAmount <= BigInt::from(0) || preinit_parameters.offering.delphiCategory != DELPHI_TOKEN_ID || - preinit_parameters.offering.platformFeeNFTH != PLATFORM_FEE_NFTH || - preinit_parameters.offering.platformFeeNumerator < PLATFORM_FEE || - preinit_parameters.offering.executionFee < ENTRY_EXECUTION_FEE) { + preinit_parameters.offering.platformFeeNFTH != platform_fee_nfth || + preinit_parameters.offering.platformFeeNumerator < *PLATFORM_FEE || + preinit_parameters.offering.executionFee < *ENTRY_EXECUTION_FEE { is_valid_ido = false; } } _ => { - return Err("invalid announcement (2)"); + return Err(anyhow::anyhow!("invalid announcement (2)")); } } + } else { + return Err(anyhow::anyhow!("announcement output is missing")); } let preInitOut = tx.output.get(1); - if preInitOut.is_none() { - return Err("preinit output is not defined!"); - } - - // find authguardCategory - let authguardNftOut = None; - if preinit_parameters.authguardNftOutputIndex >= BigInt::from(0) { - authguardNftOut = tx.output.get(preinit_parameters.authguardNftOutputIndex.to_i8()); - } - - let preInit_lock_parameters = PreInitLockParameters { - authguardLockingBytecode: preinit_parameters.authguardLockingBytecode - permanentLiquidityShareNumerator: preinit_parameters.permanentLiquidityShareNumerator, - offeredTokenAmount: preinit_parameters.offeredTokenAmount, - offeredTokenTotalSupply: preinit_parameters.offeredTokenTotalSupply, - }; - let preinit_state = PreInitState { - authguardCategory: if authguardNftOut.is_some() && authguardNftOut.unwrap().token.is_some() { authguardNftOut.unwrap().token.unwrap().id.to_vec() } else { vec![0; 32] }, - idoCategory: vec![0; 32], - nextTxSetterFlag: BigInt::ZERO, - oTokenGenerated: if offered_token_is_in_supply { BigInt::from(1) } else { BigInt::ZERO }, - }; - if(build_p2sh32_script( - build_preinit_bytecode(preInit_lock_parameters, preinit_state) - ) != preInitOut.unwrap().script_pubkey) { - return Err("preInit output bytecode does not match!"); + if preInitOut.is_none() { + return Err(anyhow::anyhow!("preinit output is not defined!")); } + // find authguardCategory + let mut authguardNftOut = None; + if preinit_parameters.authguardNftOutputIndex >= BigInt::from(0) { + authguardNftOut = tx.output.get(preinit_parameters.authguardNftOutputIndex.to_usize().unwrap()); + } + + let preInit_lock_parameters = IdoPreInitLockParameters { + authguardLockingBytecode: preinit_parameters.authguardLockingBytecode.clone(), + permanentLiquidityShareNumerator: preinit_parameters.permanentLiquidityShareNumerator.clone(), + offeredTokenAmount: preinit_parameters.offeredTokenAmount.clone(), + offeredTokenTotalSupply: preinit_parameters.offeredTokenTotalSupply.clone(), + }; + + let preinit_state = IdoPreInitState { + authguardCategory: if authguardNftOut.is_some() && authguardNftOut.unwrap().token.is_some() { authguardNftOut.unwrap().token.as_ref().unwrap().id.to_blob() } else { vec![0; 32] }, + idoCategory: vec![0; 32], + nextTxSetterFlag: BigInt::from(0), + oTokenGenerated: if offered_token_is_in_supply { BigInt::from(1) } else { BigInt::from(0) }, + }; + if build_p2sh32_script( + &build_preinit_bytecode(&preInit_lock_parameters, &preinit_state) + ) != preInitOut.unwrap().script_pubkey { + return Err(anyhow::anyhow!("preInit output bytecode does not match!")); + } let offeringBytecodeStorageOut = tx.output.get(2); if offeringBytecodeStorageOut.is_none() { - return Err("offering bytecode storage not defined!"); + return Err(anyhow::anyhow!("offering bytecode storage not defined!")); } - if(build_p2sh32_script( - build_storage_script_with_data( + if build_p2sh32_script( + &build_storage_script_with_data( 1, - build_offering_bytecode( - preinit_parameters.offering.offer.maxDiscountRateNumerator, - preinit_parameters.offering.offer.discountAnnualRateNumerator, - preinit_parameters.offering.offer.priceNumerator, - preinit_parameters.offering.offer.minOffer, - preinit_parameters.offering.offer.xTokenCategory + &build_offering_bytecode( + &preinit_parameters.offering.offer.maxDiscountRateNumerator, + &preinit_parameters.offering.offer.discountAnnualRateNumerator, + &preinit_parameters.offering.offer.priceNumerator, + &preinit_parameters.offering.offer.minOffer, + preinit_parameters.offering.offer.xTokenCategory.as_deref().unwrap_or(&[]), ) - ) - ) != offeringBytecodeStorageOut.unwrap().script_pubkey) { - return Err("on_create_info, offering bytecode storage does not match!"); + ).to_bytes() + ) != offeringBytecodeStorageOut.unwrap().script_pubkey { + return Err(anyhow::anyhow!("on_create_info, offering bytecode storage does not match!")); } let launcherBytecodeStorageOut = tx.output.get(3); if launcherBytecodeStorageOut.is_none() { - return Err("on_create_info, launcher bytecode storage not defined!"); + return Err(anyhow::anyhow!("on_create_info, launcher bytecode storage not defined!")); } - if(build_p2sh32_script( - build_storage_script_with_data( + if build_p2sh32_script( + &build_storage_script_with_data( 1, - build_launcher_bytecode( - vec![0u8; 32], // collectorNFTH - preinit_parameters.offering.platformFeeNFTH, - preinit_parameters.offering.platformFee, - preinit_parameters.offering.delphiCategory, - preinit_parameters.offering.launchConditions.expiresAt, - preinit_parameters.offering.launchConditions.deployThreshold, - preinit_parameters.offering.launchConditions.immediateDeployThreshold, + &build_launcher_bytecode( + &vec![0u8; 32], // collectorNFTH + &preinit_parameters.offering.platformFeeNFTH, + &preinit_parameters.offering.platformFeeNumerator, + &preinit_parameters.offering.delphiCategory, + &preinit_parameters.offering.launchConditions.expiresAt, + &preinit_parameters.offering.launchConditions.deployThreshold, + &preinit_parameters.offering.launchConditions.immediateDeployThreshold, ) - ) - ) != launcherBytecodeStorageOut.unwrap().script_pubkey) { - return Err("launcher bytecode storage does not match!"); + ).to_bytes() + ) != launcherBytecodeStorageOut.unwrap().script_pubkey { + return Err(anyhow::anyhow!("launcher bytecode storage does not match!")); } let distDeployBytecodeStorageOut = tx.output.get(4); - if launcherBytecodeStorageOut.is_none() { - return Err("dist deploy bytecode storage not defined!"); + if distDeployBytecodeStorageOut.is_none() { + return Err(anyhow::anyhow!("dist deploy bytecode storage not defined!")); } - if(build_p2sh32_script( - build_storage_script_with_data( + if build_p2sh32_script( + &build_storage_script_with_data( 1, - DISTRIBUTOR_DEPLOY_CONTRACT, - ) - ) != distDeployBytecodeStorageOut.unwrap().script_pubkey) { - return Err("dist deploy bytecode storage does not match!"); + &DISTRIBUTOR_DEPLOY_CONTRACT, + ).to_bytes() + ) != distDeployBytecodeStorageOut.unwrap().script_pubkey { + return Err(anyhow::anyhow!("dist deploy bytecode storage does not match!")); } let distRefundBytecodeStorageOut = tx.output.get(5); - if launcherBytecodeStorageOut.is_none() { - return Err("dist refund bytecode storage not defined!"); + if distRefundBytecodeStorageOut.is_none() { + return Err(anyhow::anyhow!("dist refund bytecode storage not defined!")); } - if(build_p2sh32_script( - build_storage_script_with_data( + if build_p2sh32_script( + &build_storage_script_with_data( 1, - DISTRIBUTOR_REFUND_CONTRACT, - ) - ) != distRefundBytecodeStorageOut.unwrap().script_pubkey) { - return Err("dist refund bytecode storage does not match!"); + &DISTRIBUTOR_REFUND_CONTRACT, + ).to_bytes() + ) != distRefundBytecodeStorageOut.unwrap().script_pubkey { + return Err(anyhow::anyhow!("dist refund bytecode storage does not match!")); } let offeringInitiatorBytecodeStorageOut = tx.output.get(6); if offeringInitiatorBytecodeStorageOut.is_none() { - return Err("dist refund bytecode storage not defined!"); + return Err(anyhow::anyhow!("offering initiator bytecode storage not defined!")); } - if(build_p2sh32_script( - build_storage_script_with_data( + if build_p2sh32_script( + &build_storage_script_with_data( 1, - build_partial_offering_initiator_bytecode( - BigInt::from(6), // extensionOutpointIndex - BigInt::from(5), // tokenStorageOutpointIndex - BigInt::from(4), // distRefundOutpointIndex - BigInt::from(3), // distDeployOutpointIndex - BigInt::from(2), // launcherBytecodeOutpointIndex - BigInt::from(1), // offeringBytecodeOutpointIndex - preinit_parameters.offering.executionFee, + &build_partial_offering_initiator_bytecode( + &BigInt::from(6i64), // extensionOutpointIndex + &BigInt::from(5i64), // tokenStorageOutpointIndex + &BigInt::from(4i64), // distRefundOutpointIndex + &BigInt::from(3i64), // distDeployOutpointIndex + &BigInt::from(2i64), // launcherBytecodeOutpointIndex + &BigInt::from(1i64), // offeringBytecodeOutpointIndex + &preinit_parameters.offering.executionFee, ), - ) - ) != offeringInitiatorBytecodeStorageOut.unwrap().script_pubkey) { - return Err("offering initiator bytecode storage does not match!"); + ).to_bytes() + ) != offeringInitiatorBytecodeStorageOut.unwrap().script_pubkey { + return Err(anyhow::anyhow!("offering initiator bytecode storage does not match!")); } let idoInitiatorBytecodeStorageOut = tx.output.get(7); if idoInitiatorBytecodeStorageOut.is_none() { - return Err("dist refund bytecode storage not defined!"); + return Err(anyhow::anyhow!("ido initiator bytecode storage not defined!")); } - if(build_p2sh32_script( - build_storage_script_with_data( + if build_p2sh32_script( + &build_storage_script_with_data( 1, - build_partial_ido_initaitor_bytecode( - build_partial_postlaunch_bytecode( - permanentLiquidityShare: preinit_parameters.permanentLiquidityShareNumerator, - price: preinit_parameters.offering.offer.priceNumerator, + &build_partial_ido_initaitor_bytecode( + &build_partial_postlaunch_bytecode( + &preinit_parameters.permanentLiquidityShareNumerator, + &preinit_parameters.offering.offer.priceNumerator, ), - permanentPoolOTokenReserveOutpointIndex: BigInt::from(7), - offeringInitiatorOutpointIndex: BigInt::from(0), + &BigInt::from(7i64), // permanentPoolOTokenReserveOutpointIndex + &BigInt::from(0i64), // offeringInitiatorOutpointIndex ), - ) - ) != idoInitiatorBytecodeStorageOut.unwrap().script_pubkey) { - return Err("ido initiator bytecode storage does not match!"); + ).to_bytes() + ) != idoInitiatorBytecodeStorageOut.unwrap().script_pubkey { + return Err(anyhow::anyhow!("ido initiator bytecode storage does not match!")); } - let offered_token_id: Option> = None; + let mut offered_token_id: Option> = None; if offered_token_is_in_supply { // verify offered token storage let offeredTokenStorageOut = tx.output.get(8); if offeredTokenStorageOut.is_none() { // valid = false; - return Err("offered token supply not found!"); + return Err(anyhow::anyhow!("offered token supply not found!")); } - let permanentLiquidityOTokenReserve: BigInt = preinit_parameters.offeredTokenAmount * parameters.permanentLiquidityShareNumerator / PERMANENT_LIQUIDITY_SHARE_DENOMINATOR; - let requiredTokens = preinit_parameters.offeredTokenAmount + permanentLiquidityOTokenReserve; + let permanentLiquidityOTokenReserve: BigInt = &preinit_parameters.offeredTokenAmount * &preinit_parameters.permanentLiquidityShareNumerator / &PERMANENT_LIQUIDITY_SHARE_DENOMINATOR.clone(); + let requiredTokens = &preinit_parameters.offeredTokenAmount + permanentLiquidityOTokenReserve; if offeredTokenStorageOut.unwrap().token.is_none() { - return Err("offered token storage does not contain a token!"); + return Err(anyhow::anyhow!("offered token storage does not contain a token!")); } - if(build_p2sh32_script(build_storage_script(1)) != - offeredTokenStorageOut.unwrap().script_pubkey()) { - return Err("offered token locking bytecode does not match!"); + if build_p2sh32_script(&build_storage_script(1u32).to_bytes()) != + offeredTokenStorageOut.unwrap().script_pubkey { + return Err(anyhow::anyhow!("offered token locking bytecode does not match!")); } - if offeredTokenStorageOut.unwrap().token.unwrap().amount < requiredTokens { - return Err("invalid offered token amount!"); + let token_amount = BigInt::from(offeredTokenStorageOut.unwrap().token.as_ref().unwrap().amount); + if token_amount < requiredTokens { + return Err(anyhow::anyhow!("invalid offered token amount!")); } - offered_token_id = Some(offeredTokenStorageOut.unwrap().token.unwrap().id.to_vec()); + offered_token_id = Some(offeredTokenStorageOut.unwrap().token.as_ref().unwrap().id.to_blob()); } - + // collect preinit fee - let preinit_paid_fee: u64 = 0; + let mut preinit_paid_fee: u64 = 0; for output in &tx.output { - if(build_p2sh32_script( - build_p2nfth_script(PLATFORM_FEE_NFTH) - ) == output.script_pubkey) { + if build_p2sh32_script( + &build_p2nfth_script(&platform_fee_nfth).to_bytes() + ) == output.script_pubkey { preinit_paid_fee += output.value; } } - if(preinit_paid_fee < IDO_CREATE_EXECUTION_FEE) { + if BigInt::from(preinit_paid_fee) < *IDO_CREATE_EXECUTION_FEE { is_valid_ido = false; - info("ido parse, not enough preinit fee paid!"); + info!("ido parse, not enough preinit fee paid!"); } Ok(IdoContext { preinit_txid: tx.txid().to_blob(), init_txid: None, launch_txid: None, - status: "PREINIT", + status: "PREINIT".to_string(), parameters: IdoParameters::PreInit(preinit_parameters), state: IdoState::PreInit(preinit_state), is_valid_ido: is_valid_ido, @@ -1001,15 +1092,16 @@ async fn create_ido_context_from_preinit(tx) -> Result { } async fn on_create_ido( + network: Option, pool: &SqlitePool, tx: &Transaction, ) -> Result<()> { - match create_ido_context_from_preinit(tx) { - Ok(context) { + match create_ido_context_from_preinit(network, tx).await { + Ok(context) => { // create the ido let result = sqlx::query( "INSERT INTO ido (preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, status, parameters, state, is_valid, txchain_entrypoint, txchain_head) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(context.preinit_txid) .bind(context.init_txid) @@ -1017,47 +1109,47 @@ async fn on_create_ido( .bind(context.offering_token_id) .bind(context.offered_token_id) .bind(context.status) - .bind(json(context.parameters).to_vec()) - .bind(json(context.state).to_vec()) + .bind(serde_json::to_vec(&context.parameters).unwrap()) + .bind(serde_json::to_vec(&context.state).unwrap()) .bind(context.is_valid_ido) - .bind(None) - .bind(None) + .bind(None::) + .bind(None::) .execute(pool) .await; match result { Ok(r) => { // insert txchain entrypoint - let ido_id = r.last_insert_id(); + let ido_id = r.last_insert_rowid(); let serialized_tx = bitcoincash::consensus::serialize(tx); let result2 = sqlx::query( - "INSERT ido (prev_id, ido_id, txid, next_output_index, tx) - VALUES (?, ?, ?, ?, ?, ?)", + "INSERT INTO ido_txchain (prev_id, ido_id, txid, tx) + VALUES (?, ?, ?, ?)", ) - .bind(None) + .bind(None::) .bind(ido_id) .bind(tx.txid().to_blob()) - .bind(1) .bind(serialized_tx) .execute(pool) .await; match result2 { Ok(r2) => { // update txchain_entrypoint & txchain_head for the ido - let item_id = r2.last_insert_id(); + let item_id = r2.last_insert_rowid(); sqlx::query( - "UPDATE ido_txchain SET txchain_entrypoint = ?, txchain_head = ? + "UPDATE ido SET txchain_entrypoint = ?, txchain_head = ? WHERE id = ?", ) .bind(item_id) .bind(item_id) .bind(ido_id) .execute(pool) - .await; + .await?; + Ok(()) } - Err(e) => Err(e), + Err(e) => Err(e.into()), } } - Err(e) => Err(e), + Err(e) => Err(e.into()), } }, Err(e) => Err(e), @@ -1065,11 +1157,11 @@ async fn on_create_ido( } -fn is_preinit_state_ready_to_init(state: IdoPreInitState) -> bool { - state.oTokenGeneratedData != BigInt::ZERO && - state.nextTxSetterFlag == BigInt::ZERO && - vm_number_to_bigint(state.authguardCategory) != BigInt::ZERO && - vm_number_to_bigint(state.idoCategory) != BigInt::ZERO; +fn is_preinit_state_ready_to_init(state: &IdoPreInitState) -> bool { + state.oTokenGenerated != BigInt::from(0) && + state.nextTxSetterFlag == BigInt::from(0) && + vm_number_to_bigint(&state.authguardCategory) != BigInt::from(0) && + vm_number_to_bigint(&state.idoCategory) != BigInt::from(0) } fn parse_preinit_state_from_tx( @@ -1079,55 +1171,69 @@ fn parse_preinit_state_from_tx( let preInitIn = tx.input.get(1); if preInitIn.is_none() { // panic - return Err("Panic, Not a valid preinit tx!"); + return Err(anyhow::anyhow!("Not a valid preinit tx!")); } - let instructions = preInitIn.unwrap().script_sig.instructions(); - match instructions[0] { - Ok(Instruction::PushBytes(stateOTokenGeneratedData)) => { - match instructions[1] { - Ok(Instruction::PushBytes(stateNextTxSetterFlag)) => { - match instructions[2] { - Ok(Instruction::PushBytes(stateIdoCategory)) => { - match instructions[3] { - Ok(Instruction::PushBytes(stateAuthguardCategory)) => { - IdoPreInitState { - authguardCategory: stateAuthguardCategory, - idoCategory: stateIdoCategory, - nextTxSetterFlag: vm_number_to_bigint(stateNextTxSetterFlag), - oTokenGeneratedData: vm_number_to_bigint(stateOTokenGeneratedData), - }; - }, - _ => Err("Panic, Not a valid preinit tx input#1 (push#2)"), - } - }, - _ => Err("Panic, Not a valid preinit tx input#1 (push#2)"), - } - }, - _ => Err("Panic, Not a valid preinit tx input#1 (push#1)"), - } - }, - _ => Err("Panic, Not a valid preinit tx input#1 (push#0)"), + let unlocking_instructions: Vec<_> = preInitIn.unwrap().script_sig.instructions().collect(); + match unlocking_instructions.last() { + Some(Ok(Instruction::PushBytes(redeem_bytecode))) => { + let redeem_script = Script::from(redeem_bytecode.to_vec()); + let instructions: Vec<_> = redeem_script.instructions().collect(); + if instructions.len() < 4 { + return Err(anyhow::anyhow!("preInitIn redeem script has less than 4 opcodes!")); + } + match &instructions[0] { + Ok(Instruction::PushBytes(stateOTokenGeneratedData)) => { + match &instructions[1] { + Ok(Instruction::PushBytes(stateNextTxSetterFlag)) => { + match &instructions[2] { + Ok(Instruction::PushBytes(stateIdoCategory)) => { + match &instructions[3] { + Ok(Instruction::PushBytes(stateAuthguardCategory)) => { + Ok(IdoPreInitState { + authguardCategory: stateAuthguardCategory.to_vec(), + idoCategory: stateIdoCategory.to_vec(), + nextTxSetterFlag: vm_number_to_bigint(stateNextTxSetterFlag), + oTokenGenerated: vm_number_to_bigint(stateOTokenGeneratedData), + }) + }, + _ => Err(anyhow::anyhow!("Not a valid preinit tx input#1 (push#2)")), + } + }, + _ => Err(anyhow::anyhow!("Not a valid preinit tx input#1 (push#2)")), + } + }, + _ => Err(anyhow::anyhow!("Not a valid preinit tx input#1 (push#1)")), + } + }, + _ => Err(anyhow::anyhow!("Not a valid preinit tx input#1 (push#0)")), + } + } + _ => Err(anyhow::anyhow!("Expecting the last opcode of the unlocking script to be a oppush")), } } +#[derive(Clone)] +struct IdoUpdateEntry { + txid: Vec, + owner_nfthash: Vec, + supply_amount: u64, + demand_amount: u64, + lockup_timeval: u64, + discount: u64, + commitment: Vec, + distributed: bool, +} + +#[derive(Clone)] enum IdoUpdate { InitTxId(Vec), LaunchTxId(Vec), Status(String), State(IdoState), Parameters(IdoParameters), - OfferredTokenId(Vec), + OfferedTokenId(Vec), OfferingTokenId(Vec), - Entry { - txid: Vec, - owner_nfthash: Vec, - supply_amount: u64, - demand_amount: u64, - lockup_timeval: u64, - discount: u64, - commitment: Vec, - distributed: bool, - }, + Entry(IdoUpdateEntry), EntryDistributed { txid: Vec, }, @@ -1139,144 +1245,148 @@ struct IdoAddResult { } fn ido_add_tx( - context: IdoContext, + context: &IdoContext, tx: &Transaction, ) -> Result { // inputs to check #0, #1, #3 - let updates: Vec = Vec::new(); + let mut updates: Vec = Vec::new(); match context.status.as_str() { "PREINIT" => { - if is_preinit_state_ready_to_init(context.state) { - // init outputs created & init tx - updates.push(IdoUpdate::Status("ACTIVE")); - updates.push(IdoUpdate::InitTxId(tx.txid().to_blob())); - let launcherBytecodeIn = tx.output.get(2).expect("launcher bytecode storage does not exist!"); - let launcher_inst_list = Script(extract_data_from_unlocking_bytecode_of_storage_with_data(launcherBytecodeIn.script_sig).expect("failed to extract launcher bytecode")).instructions(); - let collectorNFTH: Vec; - match launcher_inst_list[0] { - Ok(Instruction::PushBytes(data)) => { - collectorNFTH = data; + if let IdoState::PreInit(ref preinit_state) = context.state { + if is_preinit_state_ready_to_init(preinit_state) { + // init outputs created & init tx + updates.push(IdoUpdate::Status("ACTIVE".to_string())); + updates.push(IdoUpdate::InitTxId(tx.txid().to_blob())); + let launcherBytecodeIn = tx.input.get(2).expect("launcher bytecode storage does not exist!"); + let launcher_data = extract_data_from_unlocking_bytecode_of_storage_with_data(launcherBytecodeIn.script_sig.clone()).expect("failed to extract launcher bytecode"); + let launcher_script = Script::from(launcher_data); + let launcher_inst_list: Vec<_> = launcher_script.instructions().collect(); + let collectorNFTH: Vec; + match &launcher_inst_list[0] { + Ok(Instruction::PushBytes(data)) => { + collectorNFTH = data.to_vec(); + } + _ => return Err(anyhow::anyhow!("expecting push opcode for collectorNFTH")), } - _ => return Err("expecting push opcode for collectorNFTH"), - } - let preinit_params: IdoPreInitParameters; - match context.parameters { - IdoParameters::IdoPreInitParameters(value) => { - preinit_params = value; + let preinit_params: &IdoPreInitParameters; + match &context.parameters { + IdoParameters::PreInit(value) => { + preinit_params = value; + } + _ => return Err(anyhow::anyhow!("expecting PreInit parameters")), } - _ => return Err("expecting push opcode for collectorNFTH"), + updates.push(IdoUpdate::Parameters(IdoParameters::Active(IdoActiveParameters { + collectorNFTH: collectorNFTH, + permanentLiquidityShareNumerator: preinit_params.permanentLiquidityShareNumerator.clone(), + offeredTokenAmount: preinit_params.offeredTokenAmount.clone(), + offering: preinit_params.offering.clone(), + }))); + updates.push(IdoUpdate::State(IdoState::Active(IdoActiveState { + counter: BigInt::from(0) + }))); + let tokenStorageOut = tx.output.get(2).expect("token storage does not exist!"); + updates.push(IdoUpdate::OfferedTokenId(tokenStorageOut.token.as_ref().unwrap().id.to_blob())); + let offeringOut = tx.output.get(1).expect("offering does not exist!"); + updates.push(IdoUpdate::OfferingTokenId(offeringOut.token.as_ref().unwrap().id.to_blob())); + return Ok(IdoAddResult { + updates: updates, + next_output_index: 0, // offering utxo + }); } - updates.push(IdoUpdate::Parameters(IdoParameters::Active { - collectorNFTH: collectorNFTH, - permanentLiquidityShareNumerator: preinit_params.permanentLiquidityShareNumerator, - offeredTokenAmount: preinit_params.offeredTokenAmount, - offering: preinit_params.offering, - })); - updates.push(IdoUpdate::State(IdoState::Active { - counter: BigInt::ZERO - })); - let tokenStorageOut = tx.output.get(2).expect("token storage does not exist!"); - updates.push(IdoUpdate::OfferedTokenId(tokenStorageOut.token.unwrap().id.to_vec())); - let offeringOut = tx.output.get(1).expect("offering does not exist!"); - updates.push(IdoUpdate::OfferingTokenId(offeringOut.token.unwrap().id.to_vec())); - return Ok(IdoAddResult { - updates: updates, - next_output_index: 0, // offering utxo - }); } else { - match parse_preinit_state_from_tx(tx) { - Ok(new_state) => { - updates.push(IdoUpdate::State(IdoState::PreInit(new_state))); - return Ok(IdoAddResult { - updates: updates, - next_output_index: if is_preinit_state_ready_to_init(new_state) { 0 } else { 1 }, - }); - }, - Err(e) => return Err("parse_preinit_state failed, {}", e), - } + return Err(anyhow::anyhow!("expecting PreInitState!")); + } + match parse_preinit_state_from_tx(tx) { + Ok(new_state) => { + let ready = is_preinit_state_ready_to_init(&new_state); + updates.push(IdoUpdate::State(IdoState::PreInit(new_state))); + return Ok(IdoAddResult { + updates: updates, + next_output_index: if ready { 0 } else { 1 }, + }); + }, + Err(e) => return Err(anyhow::anyhow!("parse_preinit_state failed: {}", e)), } }, "ACTIVE" => { // output#0 should contain a token with offering_token_id let first_output = tx.output.get(0).expect("Should have first output!"); - if(first_output.token.is_none() || - first_output.token.unwrap().id != context.offering_token_id || - !first_output.token.unwrap().has_nft()) { - return Err("output#0 is not one of the offering nft!"); + if first_output.token.is_none() || + first_output.token.as_ref().unwrap().id.to_blob() != context.offering_token_id.clone().unwrap_or_default() || + !first_output.token.as_ref().unwrap().has_nft() { + return Err(anyhow::anyhow!("output#0 is not one of the offering nft!")); } - if first_output.token.unwrap().commitment[0] & ITEM_TYPE_BITS == ITEM_TYPE_OFFERING { + if first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS == ITEM_TYPE_OFFERING { // enforce limitation (xToken == NATIVE_BCH) - if context.parameters.offering.offer.xTokenCategory.is_some() { - return Err("non-null xTokenCategory is not supported"); + if let IdoParameters::Active(ref active_params) = context.parameters { + if active_params.offering.offer.xTokenCategory.is_some() { + return Err(anyhow::anyhow!("non-null xTokenCategory is not supported")); + } } let second_output = tx.output.get(1).expect("Should have the second output!"); - if(second_output.token.is_none() || - - first_output.token.unwrap().id != context.offering_token_id || - !second_output.token.unwrap().has_nft()) { - return Err("output#1 should be an offering entry nft!"); + if second_output.token.is_none() || + second_output.token.as_ref().unwrap().id.to_blob() != context.offering_token_id.clone().unwrap_or_default() || + !second_output.token.as_ref().unwrap().has_nft() { + return Err(anyhow::anyhow!("output#1 should be an offering entry nft!")); } // input#0 is the offering, add entry // pull owner_nfthash from unlocking bytecode of input#0 let owner_nfthash; - let instructions = tx.input.get(0).expect("Should have first input!").script_sig.instructions(); - match instructions[0] { + let instructions: Vec<_> = tx.input.get(0).expect("Should have first input!").script_sig.instructions().collect(); + match &instructions[0] { Ok(Instruction::PushBytes(data)) => { - owner_nfthash = data; + owner_nfthash = data.to_vec(); }, - _ => return Err("OP_PUSH expected in the unlocking bytecode of the offering utxo!"), + _ => return Err(anyhow::anyhow!("OP_PUSH expected in the unlocking bytecode of the offering utxo!")), } - updates.push(IdoUpdate::Entry { + updates.push(IdoUpdate::Entry(IdoUpdateEntry { txid: tx.txid().to_blob(), owner_nfthash: owner_nfthash, supply_amount: second_output.value, - demand_amount: second_output.token.unwrap().amount, - lockup_timeval: decode_padded_vm_number(second_output.token.unwrap().commitment[1..7]).to_u64().unwrap(), - discount: decode_padded_vm_number(second_output.token.unwrap().commitment[7..15]).to_u64().unwrap(), - commitment: second_output.token.unwrap().commitment, + demand_amount: second_output.token.as_ref().unwrap().amount as u64, + lockup_timeval: decode_padded_vm_number(&second_output.token.as_ref().unwrap().commitment[1..7]).to_u64().unwrap_or(0), + discount: decode_padded_vm_number(&second_output.token.as_ref().unwrap().commitment[7..15]).to_u64().unwrap_or(0), + commitment: second_output.token.as_ref().unwrap().commitment.clone(), distributed: false, - }); - updates.push(IdoUpdate::State(IdoState::Active { - counter: decode_padded_vm_number(first_output.token.unwrap().commitment[5..9]), })); - } else if (first_output.token.unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_DISTRIBUTOR { + updates.push(IdoUpdate::State(IdoState::Active(IdoActiveState { + counter: decode_padded_vm_number(&first_output.token.as_ref().unwrap().commitment[5..9]), + }))); + } else if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_DISTRIBUTOR { // input#0 is the launcher // output#0 distributor - let dist_commitment = first_output.token.unwrap().commitment; - updates.push(IdoUpdate::Status("DISTRIBUTING")); + let dist_commitment = first_output.token.as_ref().unwrap().commitment.clone(); + updates.push(IdoUpdate::Status("DISTRIBUTING".to_string())); updates.push(IdoUpdate::LaunchTxId(tx.txid().to_blob())); updates.push(IdoUpdate::State( IdoState::Distributing(IdoDistributingState { isRefund: (dist_commitment[0] & DISTRIBUTOR_FLAG_IS_REFUND) != 0, - timestamp: decode_padded_vm_number(dist_commitment[1..7]), - counter: decode_padded_vm_number(dist_commitment[7..11]), - earnedAmount: decode_padded_vm_number(dist_commitment[11..19]), - refundAmount: decode_padded_vm_number(dist_commitment[19..27]), - discountAmount: decode_padded_vm_number(dist_commpitment[27..35]), - platformEarnedAmount: BigInt::ZERO, + timestamp: decode_padded_vm_number(&dist_commitment[1..7]), + counter: decode_padded_vm_number(&dist_commitment[7..11]), + earnedAmount: decode_padded_vm_number(&dist_commitment[11..19]), + refundAmount: decode_padded_vm_number(&dist_commitment[19..27]), + discountAmount: decode_padded_vm_number(&dist_commitment[27..35]), + platformEarnedAmount: BigInt::from(0), }) )); - } else if (first_output.token.unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_CONFIRMATION_NFT { + } else if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_CONFIRMATION_NFT { // input#0 is the launcher, nodist, jump to postlaunch // output#0 is the confirmation nft - let conf_commitment = first_output.token.unwrap().commitment; - updates.push(IdoUpdate::Status("POSTLAUNCH")); + let conf_commitment = first_output.token.as_ref().unwrap().commitment.clone(); + updates.push(IdoUpdate::Status("POSTLAUNCH".to_string())); updates.push(IdoUpdate::LaunchTxId(tx.txid().to_blob())); updates.push(IdoUpdate::State( IdoState::PostLaunch(IdoPostLaunchState { isRefund: (conf_commitment[0] & CONFIRMATION_NFT_FLAG_IS_REFUND) != 0, - timestamp: decode_padded_vm_number(conf_commitment[1..7]), - earnedAmount: decode_padded_vm_number(conf_commitment[7..15]), - refundAmount: decode_padded_vm_number(conf_commitment[15..23]), - discountAmount: decode_padded_vm_number(conf_commitment[23..31]), - platformEarnedAmount: BigInt::ZERO, - postInitInitialized: false, - collectedBCH: 0, - collectedOToken: 0, + timestamp: decode_padded_vm_number(&conf_commitment[1..7]), + earnedAmount: decode_padded_vm_number(&conf_commitment[7..15]), + refundAmount: decode_padded_vm_number(&conf_commitment[15..23]), + discountAmount: decode_padded_vm_number(&conf_commitment[23..31]), + platformEarnedAmount: BigInt::from(0), }) )); } else { - return Err("output#0 is not recognized!"); + return Err(anyhow::anyhow!("output#0 is not recognized!")); } return Ok(IdoAddResult { updates: updates, @@ -1284,55 +1394,60 @@ fn ido_add_tx( }); }, "DISTRIBUTING" => { - // enforce limitation (xToken == NATIVE_BCH) - if preinit_parameters.offering.offer.xTokenCategory.is_some() { - return Err("non-null xTokenCategory is not supported"); + match &context.parameters { + IdoParameters::Active(parameters) => { + // enforce limitation (xToken == NATIVE_BCH) + if parameters.offering.offer.xTokenCategory.is_some() { + return Err(anyhow::anyhow!("non-null xTokenCategory is not supported")); + } + } + _ => return Err(anyhow::anyhow!("expecting active parameters")), } // output#0 should contain a token with offering_token_id let first_output = tx.output.get(0).expect("Should have first output!"); - if(first_output.token.is_none() || - first_output.token.unwrap().id != context.offering_token_id || - !first_output.token.unwrap().has_nft()) { - return Err("output#0 is not one of the offering nft!"); + if first_output.token.is_none() || + first_output.token.as_ref().unwrap().id.to_blob() != context.offering_token_id.clone().unwrap_or_default() || + !first_output.token.as_ref().unwrap().has_nft() { + return Err(anyhow::anyhow!("output#0 is not one of the offering nft!")); } - let prev_state: IdoDistributingState; - match context.state { + let prev_state: &IdoDistributingState; + match &context.state { IdoState::Distributing(value) => { prev_state = value; } - _ => return Err("expecting distributing state"), + _ => return Err(anyhow::anyhow!("expecting distributing state")), } let platform_fee_output = tx.output.get(4).expect("platform fee output does not exist!"); - if (first_output.token.unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_DISTRIBUTOR { + if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_DISTRIBUTOR { // output#0 distributor - let dist_commitment = first_output.token.unwrap().commitment; + let dist_commitment = first_output.token.as_ref().unwrap().commitment.clone(); updates.push(IdoUpdate::State( IdoState::Distributing(IdoDistributingState { isRefund: (dist_commitment[0] & DISTRIBUTOR_FLAG_IS_REFUND) != 0, - timestamp: decode_padded_vm_number(dist_commitment[1..7]), - counter: decode_padded_vm_number(dist_commitment[7..11]), - earnedAmount: decode_padded_vm_number(dist_commitment[11..19]), - refundAmount: decode_padded_vm_number(dist_commitment[19..27]), - discountAmount: decode_padded_vm_number(dist_commpitment[27..35]), - platformEarnedAmount: prev_state.platformEarnedAmount + BigInt::from(platform_fee_output.value), + timestamp: decode_padded_vm_number(&dist_commitment[1..7]), + counter: decode_padded_vm_number(&dist_commitment[7..11]), + earnedAmount: decode_padded_vm_number(&dist_commitment[11..19]), + refundAmount: decode_padded_vm_number(&dist_commitment[19..27]), + discountAmount: decode_padded_vm_number(&dist_commitment[27..35]), + platformEarnedAmount: &prev_state.platformEarnedAmount + BigInt::from(platform_fee_output.value), }) )); - } else if (first_output.token.unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_CONFIRMATION_NFT { + } else if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_CONFIRMATION_NFT { // output#0 is the confirmation nft - let conf_commitment = first_output.token.unwrap().commitment; - updates.push(IdoUpdate::Status("POSTLAUNCH")); + let conf_commitment = first_output.token.as_ref().unwrap().commitment.clone(); + updates.push(IdoUpdate::Status("POSTLAUNCH".to_string())); updates.push(IdoUpdate::State( IdoState::PostLaunch(IdoPostLaunchState { isRefund: (conf_commitment[0] & CONFIRMATION_NFT_FLAG_IS_REFUND) != 0, - timestamp: decode_padded_vm_number(conf_commitment[1..7]), - earnedAmount: decode_padded_vm_number(conf_commitment[7..15]), - refundAmount: decode_padded_vm_number(conf_commitment[15..23]), - discountAmount: decode_padded_vm_number(conf_commitment[23..31]), - platformEarnedAmount: prev_state.platformEarnedAmount + BigInt::from(platform_fee_output.value), + timestamp: decode_padded_vm_number(&conf_commitment[1..7]), + earnedAmount: decode_padded_vm_number(&conf_commitment[7..15]), + refundAmount: decode_padded_vm_number(&conf_commitment[15..23]), + discountAmount: decode_padded_vm_number(&conf_commitment[23..31]), + platformEarnedAmount: &prev_state.platformEarnedAmount + BigInt::from(platform_fee_output.value), }) )); } else { - return Err("output#0 is not recognized!"); + return Err(anyhow::anyhow!("output#0 is not recognized!")); } return Ok(IdoAddResult { updates: updates, @@ -1340,420 +1455,1032 @@ fn ido_add_tx( }); }, "POSTLAUNCH" => { - updates.push(IdoUpdate::Status("DISTRIBUTED")); - let prev_state: IdoPostLaunchState; - match context.state { + let pp_output = tx.output.get(0).expect("Should have first output!"); + let collector_p2nfth = tx.output.get(2).expect("Should have third output!"); + updates.push(IdoUpdate::Status("DISTRIBUTED".to_string())); + let prev_state: &IdoPostLaunchState; + match &context.state { IdoState::PostLaunch(value) => { prev_state = value; } - _ => return Err("expecting distributing state"), + _ => return Err(anyhow::anyhow!("expecting postlaunch state")), } updates.push(IdoUpdate::State( - IdoState::PostLaunch(IdoDistributedState { - permanentPool: IdoPermanentPoolV0 { - tokenAmount: BigInt::from(pp_output.token + IdoState::Distributed(IdoDistributedState { + permanentPool: if pp_output.token.is_none() { None } else { Some(IdoPermanentPoolV0 { + tokenAmount: BigInt::from(pp_output.token.as_ref() .expect("pp should have tokens") .amount), satoshiAmount: BigInt::from(pp_output.value), - }; - refundAmount: BigInt::from(collector_p2nfth.token + }) }, + refundAmount: BigInt::from(collector_p2nfth.token.as_ref() .expect("collector_p2nfth should have tokens") .amount), - discountAmount: prev_state.discountAmount, - collectorEarnedAmount: BigInt::from(collector_p2nfth_output.value), - platformEarnedAmount: prev_state.platformEarnedAmount, - - isRefund: (conf_commitment[0] & CONFIRMATION_NFT_FLAG_IS_REFUND) != 0, - timestamp: decode_padded_vm_number(conf_commitment[1..7]), - earnedAmount: decode_padded_vm_number(conf_commitment[7..15]), - refundAmount: decode_padded_vm_number(conf_commitment[15..23]), - discountAmount: decode_padded_vm_number(conf_commitment[23..31]), - platformEarnedAmount: prev_state.platformEarnedAmount + BigInt::from(platform_fee_output.value), + discountAmount: prev_state.discountAmount.clone(), + collectorEarnedAmount: BigInt::from(collector_p2nfth.value), + platformEarnedAmount: prev_state.platformEarnedAmount.clone(), }) )); + updates.push(IdoUpdate::EntryDistributed { + txid: tx.txid().to_blob(), + }); return Ok(IdoAddResult { updates: updates, next_output_index: -1, }); - }, - _ => return Err( - "An ido with an unknown status, id: {}, status: {}", - ido.id, - ido.status - ), + }, + _ => return Err(anyhow::anyhow!( + "An ido with an unknown status: {}", + context.status + )), } } async fn update_ido( - dbtx: &sqlx::Transaction, - ido: IdoDBRecord, - context: IdoContext, - txchain_head: u64, + dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + ido_id: i64, + context: &IdoContext, + txchain_head: i64, ) -> Result<()> { sqlx::query( "UPDATE ido set init_txid = ?, launch_txid = ?, status = ?, parameters = ?, state = ?, offering_token_id = ?, offered_token_id = ?, txchain_head = ?, is_valid = ? WHERE id = ?", ) - .bind(context.init_txid) - .bind(context.launch_txid) - .bind(context.status) - .bind(json(context.parameters).to_vec()) - .bind(json(context.state).to_vec()) - .bind(context.offering_token_id) - .bind(context.offered_token_id) + .bind(&context.init_txid) + .bind(&context.launch_txid) + .bind(&context.status) + .bind(serde_json::to_vec(&context.parameters).unwrap()) + .bind(serde_json::to_vec(&context.state).unwrap()) + .bind(&context.offering_token_id) + .bind(&context.offered_token_id) .bind(txchain_head) .bind(context.is_valid_ido) - .bind(ido.id) - .execute(dbtx) - .await; + .bind(ido_id) + .execute(&mut **dbtx) + .await?; + Ok(()) } fn apply_updates_to_context( context: &mut IdoContext, - updates: Vec, + updates: &[IdoUpdate], ) -> () { for update in updates { match update { IdoUpdate::InitTxId(txid) => { - context.init_txid = txid; + context.init_txid = Some(txid.clone()); }, IdoUpdate::LaunchTxId(txid) => { - context.launch_txid = txid; + context.launch_txid = Some(txid.clone()); }, IdoUpdate::Status(status) => { - context.status = status; + context.status = status.clone(); }, IdoUpdate::State(state) => { - context.state = state; + context.state = state.clone(); }, IdoUpdate::Parameters(parameters) => { - context.parameters = parameters; + context.parameters = parameters.clone(); }, IdoUpdate::OfferedTokenId(token_id) => { - context.offered_token_id = token_id; + context.offered_token_id = Some(token_id.clone()); }, IdoUpdate::OfferingTokenId(token_id) => { - context.offering_token_id = token_id; + context.offering_token_id = Some(token_id.clone()); }, + _ => {} } } } async fn upsert_updates_to_ido_entries( - dbtx: &sqlx::Transaction, - ido: IdoDBRecord, - updates: Vec, + dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + ido_id: i64, + updates: &[IdoUpdate], ) -> Result<()> { - let mut insert_list = Vec::new(); - let mut mark_distributed_list = Vec::new(); + let mut insert_list: Vec = Vec::new(); + let mut mark_distributed_list: Vec<&Vec> = Vec::new(); for update in updates { match update { IdoUpdate::Entry(value) => { - insert_list.push(value); - }, - IdoUpdate::EntryDistributed(value) => { - let entry = insert_list.iter().find(|&a| a.txid == value.txid); - if entry.is_some() { - entry.distributed = true; + insert_list.push(value.clone()); + }, + IdoUpdate::EntryDistributed { txid } => { + let entry = insert_list.iter_mut().find(|a| a.txid == *txid); + if let Some(e) = entry { + e.distributed = true; } else { - mark_distributed_list.push(value); + mark_distributed_list.push(txid); } }, + _ => {} } } for entry in insert_list { - match sqlx::query( - "INSERT INTO ido_entry (ido_id, txid, owner_nfthash, commitment, supply_amount, demand_amount, lockup_timeval, discount, distributed) + sqlx::query( + "INSERT INTO ido_entry (ido_id, txid, owner_nfthash, commitment, supply_amount, demand_amount, lockup_timeval, discount, distributed) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", - ) - .bind(ido.id) - .bind(entry.txid) - .bind(entry.owner_nfthash) - .bind(entry.commitment) - .bind(entry.supply_amount) - .bind(entry.demand_amount) - .bind(entry.lockup_timeval) - .bind(entry.discount) - .bind(entry.distributed) - .execute(dbtx) - .await { - Err(e) => return Err(e), - } + ) + .bind(ido_id) + .bind(entry.txid) + .bind(entry.owner_nfthash) + .bind(entry.commitment) + .bind(entry.supply_amount as i64) + .bind(entry.demand_amount as i64) + .bind(entry.lockup_timeval as i64) + .bind(entry.discount as i64) + .bind(entry.distributed) + .execute(&mut **dbtx) + .await?; } - for item in mark_distributed_list { - match sqlx::query( - "UPDATE ido_entry SET distributed = 1 WHERE txid = ?", - ) - .bind(item.txid) - .execute(dbtx) - .await { - Err(e) => return Err(e), - } + for item_txid in mark_distributed_list { + sqlx::query( + "UPDATE ido_entry SET distributed = 1 WHERE txid = ?", + ) + .bind(item_txid) + .execute(&mut **dbtx) + .await?; } + Ok(()) } async fn upsert_ido_txchain( - dbtx: &sqlx::Transaction, + dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, tx: &Transaction, + ido_id: i64, + prev_txchain_id: Option, block_height: i64, - next_output_index: i64, + next_output_index: i32, ) -> Result { let serialized_tx = bitcoincash::consensus::serialize(tx); - let txchain_item_id: i64; - match sqlx::query( + let result = sqlx::query( "REPLACE INTO ido_txchain (prev_id, ido_id, txid, tx) VALUES - (?, ?, ?, ?, ?)" + (?, ?, ?, ?)" ) - .bind(prev_txchain_item.id) - .bind(ido.id) + .bind(prev_txchain_id) + .bind(ido_id) .bind(tx.txid().to_blob()) .bind(serialized_tx) - .execute(dbtx) - .await { - Ok(r) => { - txchain_item_id = result.last_insert_id(); - }, - Err(e) => Err(e), - } + .execute(&mut **dbtx) + .await?; + let txchain_item_id: i64 = result.last_insert_rowid(); if next_output_index >= 0 { - match sqlx::query( - "REPLACE INTO ido_txchain_tracker_map (txid, next_output_index, height, txchain_id) VALUES - (?, ?, ?, ?, ?)" - ) - .bind(tx.txid().to_blob()) - .bind(next_output_index) - .bind(block_height) - .bind(txchain_item_id) - .await { - Err(e) => return Err(e), - } + sqlx::query( + "REPLACE INTO ido_txchain_tracker_map (txid, next_output_index, height, txchain_id) VALUES + (?, ?, ?, ?)" + ) + .bind(tx.txid().to_blob()) + .bind(next_output_index) + .bind(block_height) + .bind(txchain_item_id) + .execute(&mut **dbtx) + .await?; } - return txchain_item_id; + Ok(txchain_item_id) } async fn update_ido_txchain_tracker_block_height( - dbtx: &sqlx::Transaction, + dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, tx: &Transaction, block_height: i64, ) -> Result<()> { // only update block_height - match sqlx::query( - "UPDATE ido_txchain_tracker_map SET height = ? WHERE txid = ?" - ) - .bind(block_height) - .bind(tx.txid().to_blob()) - .execute(dbtx) - .await { - Ok(r) => Ok(()), - Err(e) => Err(e), - } + sqlx::query( + "UPDATE ido_txchain_tracker_map SET height = ? WHERE txid = ?" + ) + .bind(block_height) + .bind(tx.txid().to_blob()) + .execute(&mut **dbtx) + .await?; + Ok(()) } async fn on_add_ido_tx( + network: Option, pool: &SqlitePool, - ido: IdoDBRecord, - prev_txchain_item: IdoTxChainDBRecord, + ido: &IdoDBRecord, + prev_txchain_item: &IdoTxChainDBRecord, tx: &Transaction, block_height: i64, ) -> Result<()> { let mut dbtx = pool.begin().await?; - if tx.txid().to_blob() == ido.txchain_head { + let current_item = get_txchain_item_by_txid(pool, &tx.txid()) + .await?; + if current_item.is_some() && Some(current_item.unwrap().id) == ido.txchain_head { // already added, only update block height - update_ido_txchain_tracker_block_height(dbtx, tx, block_height) - .await; - } else if prev_txchain_item.id != ido.txchain_head { + update_ido_txchain_tracker_block_height(&mut dbtx, tx, block_height) + .await?; + } else if prev_txchain_item.id != ido.txchain_head.unwrap_or(0) { // rebuild the state, txchain is broken, recreate the chain - let items_result = get_ido_txchain_list(pool, ido.id) - .await; - if items_result.is_err() { - return items_result; + let items = get_ido_txchain_list(pool, ido.id) + .await?; + let mut items_tx_map: HashMap, Transaction> = HashMap::new(); + for item in &items { + let tx_deser: Transaction = bitcoincash::consensus::deserialize(&item.tx).expect("failed to deserialize tx"); + items_tx_map.insert(item.txid.clone(), tx_deser); } - let items = items_result.unwrap(); - let mut items_map_by_input = HashMap::new(); - let mut items_tx_map = HashMap::new(); - for item in items { - let tx = bitcoincash::consensus::deserialize(item.tx); - items_tx_map.insert(item.txid, tx); - for input in tx.input { - let outpoint = [input.previous_output.txid.to_hex(), input.previous_output.vout.to_string()].join(":"); - let sub_items_wrapped = items_map_by_input.get(outpoint); - if sub_items_wrapped.is_some() { - sub_items_wrapped.unwrap().push(item) - } else { - let sub_items = Vec::new(); - sub_items.push(item); - items_map_by_input.insert(outpoint, sub_items); - } - } - } - let items_copy = items.iter().copied().collect(); - // let entrypoint_item_index = items.iter().position(|&a| a.id == ido.txchain_entrypoint).except("ido's entrypoint txchain item not found!"); - // let entrypoint_item = items_copy.remove(entrypoint_item_index); - let mut new_chain = Vec::new(); - let item = prev_txchain_item; - new_chain.insert(0, item) - while item.prev_id.is_some() { - let prev_id = item.prev_id.unwrap(); - let item_index = items_copy.iter().position(|&a| a.id == prev_id).except("txchain prev item not found!"); - item = items_copy.remove(item_index); - new_chain.insert(0, item); + let mut new_chain: Vec<&IdoTxChainDBRecord> = Vec::new(); + let mut items_copy: Vec<&IdoTxChainDBRecord> = items.iter().rev().collect(); + let mut current_item = prev_txchain_item; + new_chain.insert(0, current_item); + while current_item.prev_id.is_some() { + let prev_id = current_item.prev_id.unwrap(); + let found_index = items_copy.iter().position(|a| a.id == prev_id).expect("txchain prev item not found!"); + current_item = items_copy.remove(found_index); + new_chain.insert(0, current_item); } if new_chain.get(0).unwrap().txid != ido.preinit_txid { - return Err("txchain entrypoint does not match preinit_txid"); + return Err(anyhow::anyhow!("txchain entrypoint does not match preinit_txid")); } // start from PREINIT - // Txid::from_inner() - let preinit_tx = items_tx_map.get(ido.preinit_txid).expect("ido's entrypoint does not exist in the txchain"); - match create_ido_context_from_preinit(preinit_tx) { - Ok(context) => { - let mut updates = Vec::new(); - for i in 1..new_chain.len() { - let item = new_chain.get(i).expect("should not be none!"); - let item_tx = items_tx_map.get(item.txid).expect("should not be none!"); - match ido_add_tx(context, item_tx) { - Ok(IdoAddResult(result)) => { - apply_updates_to_context(context, result.updates); - updates.append(result.updates); - } - Err(e) => return Err(e), - } - } - // add this tx - match ido_add_tx(context, tx) { - Ok(IdoAddResult(result)) => { - apply_updates_to_context(context, result.updates); - updates.append(result.updates); - match upsert_ido_txchain(dbtx, tx, block_height, result.next_output_index) - .await { - Ok(insert_id) => { - match update_ido(dbtx, ido, context, insert_id) - .await { - Err(e) => return Err(e), - } - // delete all entries - match sqlx::query( - "DELETE FROM ido_entry WHERE ido_id = ?", - ) - .bind(ido.id) - .execute(dbtx) - .await { - Ok(r) => Ok(()), - Err(e) => Err(e), - } - // insert the entries of the new state - match upsert_updates_to_ido_entries(dbtx, ido, updates) - .await { - Err(e) => return Err(e), - } - }, - Err(e) => return Err(e), - } - }, - Err(e) => return Err(e), - } - }, - Err(e) => return Err(e), - } - } else { - // create context from ido + let preinit_tx = items_tx_map.get(&ido.preinit_txid).expect("ido's entrypoint does not exist in the txchain"); + let mut context = create_ido_context_from_preinit(network, preinit_tx).await?; + let mut updates: Vec = Vec::new(); + for i in 1..new_chain.len() { + let item = new_chain.get(i).expect("should not be none!"); + let item_tx = items_tx_map.get(&item.txid).expect("should not be none!"); + // add tx + let result = ido_add_tx(&context, item_tx)?; + apply_updates_to_context(&mut context, &result.updates); + updates.extend_from_slice(&result.updates); + } + // add the tx to the chain + let result = ido_add_tx(&context, tx)?; + apply_updates_to_context(&mut context, &result.updates); + updates.extend_from_slice(&result.updates); + let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, ido.id, Some(prev_txchain_item.id), block_height, result.next_output_index) + .await?; + update_ido(&mut dbtx, ido.id, &context, txchain_item_id) + .await?; + // delete all entries + sqlx::query( + "DELETE FROM ido_entry WHERE ido_id = ?", + ) + .bind(ido.id) + .execute(&mut *dbtx) + .await?; + // insert the entries of the new state + upsert_updates_to_ido_entries(&mut dbtx, ido.id, &updates) + .await?; + } else { + // create context from ido let mut context: IdoContext = IdoContext { - preinit_txid: ido.preinit_txid, - init_txid: ido.init_txid, - launch_txid: ido.launch_txid, - status: ido.status, + preinit_txid: ido.preinit_txid.clone(), + init_txid: ido.init_txid.clone(), + launch_txid: ido.launch_txid.clone(), + status: ido.status.clone(), parameters: serde_json::from_slice(&ido.parameters).expect("Failed to parse parameters"), state: serde_json::from_slice(&ido.state).expect("Failed to parse state"), is_valid_ido: ido.is_valid, - offered_token_id: ido.offered_token_id, - offering_token_id: ido.offering_token_id, + offered_token_id: ido.offered_token_id.clone(), + offering_token_id: ido.offering_token_id.clone(), }; // add to the chain - match ido_add_tx(context, tx) { - Ok(IdoAddResult(result)) => { - apply_updates_to_context(context, result.updates); - match upsert_ido_txchain(dbtx, tx, block_height, result.next_output_index) - .await { - Ok(insert_id) => { - match update_ido(dbtx, ido, context, insert_id) - .await { - Err(e) => return Err(e), - } - match upsert_updates_to_ido_entries(dbtx, ido, result.updates) - .await { - Err(e) => return Err(e), - } - }, - Err(e) => return Err(e), - } - }, - Err(e) => return Err(e), - } - } - Ok(()); - dbtx.commit().await?; + let result = ido_add_tx(&context, tx)?; + apply_updates_to_context(&mut context, &result.updates); + let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, ido.id, Some(prev_txchain_item.id), block_height, result.next_output_index) + .await?; + update_ido(&mut dbtx, ido.id, &context, txchain_item_id) + .await?; + upsert_updates_to_ido_entries(&mut dbtx, ido.id, &result.updates) + .await?; + } + dbtx.commit().await?; + Ok(()) } -fn is_ido_sig_in_tx_inputs (txs: &Transaction) -> bool { +fn is_ido_sig_in_tx_inputs(tx: &Transaction) -> bool { for input_index in [ 0, 1 ] { let input = tx.input.get(input_index); if input.is_some() { - let instructions = input.script_sig.instructions(); - match instructions[instructions.count() - 1] { - Ok(Instruction::PushBytes(redeem_bytecode)) => { - if redeem_bytecode[0..IDO_SIGNATURE.len()] == IDO_SIGNATURE { - return true; - } - } - } + if has_script_ido_signature(&input.unwrap().script_sig) { + return true; + } } } return false; } -/// Index ido related transactions in a block pub async fn index_block( + network: Option, pool: &SqlitePool, sorted_txs: &[Transaction], - blockhash: &BlockHash, - mtp: i64, + _blockhash: &BlockHash, + _mtp: i64, block_height: i64, ) -> Result<()> { - let mut count = 0; for tx in sorted_txs { // detect a new ido if is_preinit_broadcast(tx) { - on_create_ido(pool, tx) - .await; + on_create_ido(network, pool, tx) + .await?; } else if is_ido_sig_in_tx_inputs(tx) { // has ido sig for input_index in [ 0, 1, 3 ] { let input = tx.input.get(input_index); if input.is_some() { - let result = txchain_lookup_next(pool, input.previous_output.txid, input.previous_output.vout) - .await; - match result { - Ok(Option::Some(txchain_item)) => { - let result2 = ido_lookup(pool, txchain_item.ido_id) - .await; - match result2 { - Ok(Option::Some(ido)) => { - on_add_ido_tx(pool, ido, txchain_item, tx) - .await; - }, - Err(err) => return Err(err), - _ => return Erro("ido record not found!!"), - } - break; - }, - Err(err) => return Err(err), - } + if let Some(txchain_item) = + txchain_lookup_next( + pool, + &input.unwrap().previous_output.txid, + input.unwrap().previous_output.vout + ) + .await? + { + let ido = ido_lookup(pool, txchain_item.ido_id) + .await? + .expect("ido record not found!!"); + on_add_ido_tx(network, pool, &ido, &txchain_item, tx, block_height) + .await?; + } } } } } - txchain_trim_tracker(pool, block_height) - .await; - Ok(()); + txchain_trim_tracker(pool, block_height) + .await?; + Ok(()) +} + +// ─── Public RPC types ──────────────────────────────────────────────────────── + +#[derive(Serialize, Clone)] +pub struct IdoRpcRecord { + pub id: i64, + pub preinit_txid: String, + pub init_txid: Option, + pub launch_txid: Option, + pub offering_token_id: Option, + pub offered_token_id: Option, + pub status: String, + pub parameters: serde_json::Value, + pub state: serde_json::Value, + pub txchain_entrypoint: Option, + pub txchain_head: Option, + pub is_valid: bool, +} + +#[derive(Serialize, Clone)] +pub struct IdoEntryRpcRecord { + pub ido_id: i64, + pub txid: String, + pub owner_nfthash: String, + pub commitment: Option, + pub supply_amount: i64, + pub demand_amount: i64, + pub lockup_timeval: i64, + pub discount: i64, + pub distributed: bool, +} + +#[derive(Serialize, Clone)] +pub struct IdoTxChainRpcRecord { + pub id: i64, + pub prev_id: Option, + pub ido_id: i64, + pub txid: String, +} + +#[derive(Serialize, Clone)] +pub struct IdoTrackerMapRpcRecord { + pub txid: String, + pub next_output_index: Option, + pub height: i64, + pub txchain_id: i64, +} + +impl IdoDBRecord { + fn into_rpc_record(self) -> Result { + Ok(IdoRpcRecord { + id: self.id, + preinit_txid: blob_to_display_hex::(&self.preinit_txid)?, + init_txid: self.init_txid.as_deref().map(blob_to_display_hex::).transpose()?, + launch_txid: self.launch_txid.as_deref().map(blob_to_display_hex::).transpose()?, + offering_token_id: self.offering_token_id.as_deref().map(blob_to_display_hex::).transpose()?, + offered_token_id: self.offered_token_id.as_deref().map(blob_to_display_hex::).transpose()?, + status: self.status, + parameters: serde_json::from_slice(&self.parameters)?, + state: serde_json::from_slice(&self.state)?, + txchain_entrypoint: self.txchain_entrypoint, + txchain_head: self.txchain_head, + is_valid: self.is_valid, + }) + } +} + +// ─── Public RPC query functions ────────────────────────────────────────────── + +pub async fn list_idos( + pool: &SqlitePool, + is_valid: Option, + status: Option, + offered_token_id_blob: Option>, + offset: i64, + limit: i64, +) -> Result> { + let mut qb: sqlx::QueryBuilder = sqlx::QueryBuilder::new( + "SELECT id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, \ + status, parameters, state, txchain_entrypoint, txchain_head, is_valid FROM ido WHERE 1=1", + ); + if let Some(v) = is_valid { + qb.push(" AND is_valid = "); + qb.push_bind(v as i64); + } + if let Some(v) = status { + qb.push(" AND status = "); + qb.push_bind(v); + } + if let Some(v) = offered_token_id_blob { + qb.push(" AND offered_token_id = "); + qb.push_bind(v); + } + qb.push(" ORDER BY id ASC LIMIT "); + qb.push_bind(limit); + qb.push(" OFFSET "); + qb.push_bind(offset); + + qb.build() + .fetch_all(pool) + .await? + .into_iter() + .map(|row| IdoDBRecord::from_row(&row)?.into_rpc_record()) + .collect() +} + +pub async fn get_ido_by_offering_token_id( + pool: &SqlitePool, + offering_token_id_blob: Vec, +) -> Result> { + let row = sqlx::query( + "SELECT id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, \ + status, parameters, state, txchain_entrypoint, txchain_head, is_valid \ + FROM ido WHERE offering_token_id = ?", + ) + .bind(offering_token_id_blob) + .fetch_optional(pool) + .await?; + row.map(|r| IdoDBRecord::from_row(&r)?.into_rpc_record()).transpose() +} + +pub async fn list_ido_entries( + pool: &SqlitePool, + ido_id: i64, + distributed: Option, + offset: i64, + limit: i64, +) -> Result> { + let mut qb: sqlx::QueryBuilder = sqlx::QueryBuilder::new( + "SELECT ido_id, txid, owner_nfthash, commitment, supply_amount, demand_amount, \ + lockup_timeval, discount, distributed FROM ido_entry WHERE ido_id = ", + ); + qb.push_bind(ido_id); + if let Some(v) = distributed { + qb.push(" AND distributed = "); + qb.push_bind(v as i64); + } + qb.push(" ORDER BY rowid ASC LIMIT "); + qb.push_bind(limit); + qb.push(" OFFSET "); + qb.push_bind(offset); + + qb.build() + .fetch_all(pool) + .await? + .into_iter() + .map(|row| { + let txid_blob: Vec = row.get(1); + let owner_nfthash: Vec = row.get(2); + let commitment: Option> = row.get(3); + let distributed_int: i64 = row.get(8); + Ok(IdoEntryRpcRecord { + ido_id: row.get(0), + txid: blob_to_display_hex::(&txid_blob)?, + owner_nfthash: hex::encode(&owner_nfthash), + commitment: commitment.map(|c| hex::encode(&c)), + supply_amount: row.get(4), + demand_amount: row.get(5), + lockup_timeval: row.get(6), + discount: row.get(7), + distributed: distributed_int != 0, + }) + }) + .collect() +} + +pub async fn list_ido_txchain( + pool: &SqlitePool, + ido_id: i64, + offset: i64, + limit: i64, +) -> Result> { + let head_id: Option = sqlx::query("SELECT txchain_head FROM ido WHERE id = ?") + .bind(ido_id) + .fetch_optional(pool) + .await? + .and_then(|row| row.get(0)); + + let head_id = match head_id { + Some(id) => id, + None => return Ok(vec![]), + }; + + let rows = sqlx::query( + "SELECT id, prev_id, ido_id, txid FROM ido_txchain WHERE ido_id = ?", + ) + .bind(ido_id) + .fetch_all(pool) + .await?; + + // Parse rows into records + let records: Vec = rows + .iter() + .map(|row| { + let txid_blob: Vec = row.get(3); + Ok(IdoTxChainRpcRecord { + id: row.get(0), + prev_id: row.get(1), + ido_id: row.get(2), + txid: blob_to_display_hex::(&txid_blob)?, + }) + }) + .collect::>>()?; + + // Build id → index map for O(1) lookup + let id_to_idx: HashMap = + records.iter().enumerate().map(|(i, r)| (r.id, i)).collect(); + + // Walk linked list from head backwards, then reverse to get oldest-first + let mut ordered: Vec = Vec::with_capacity(records.len()); + let mut current_id = head_id; + loop { + ordered.push(current_id); + match id_to_idx.get(¤t_id).and_then(|&i| records[i].prev_id) { + Some(prev) => current_id = prev, + None => break, + } + } + ordered.reverse(); + + let start = (offset as usize).min(ordered.len()); + let end = ((offset + limit) as usize).min(ordered.len()); + + Ok(ordered[start..end] + .iter() + .map(|id| records[id_to_idx[id]].clone()) + .collect()) +} + +pub async fn get_txchain_tx_hex( + pool: &SqlitePool, + txchain_item_id: i64, +) -> Result> { + let row = sqlx::query("SELECT tx FROM ido_txchain WHERE id = ?") + .bind(txchain_item_id) + .fetch_optional(pool) + .await?; + Ok(row.map(|r| { + let tx: Vec = r.get(0); + hex::encode(tx) + })) +} + +pub async fn list_txchain_tracker_map( + pool: &SqlitePool, + offset: i64, + limit: i64, +) -> Result> { + let rows = sqlx::query( + "SELECT txid, next_output_index, height, txchain_id \ + FROM ido_txchain_tracker_map \ + ORDER BY height ASC, txchain_id ASC \ + LIMIT ? OFFSET ?", + ) + .bind(limit) + .bind(offset) + .fetch_all(pool) + .await?; + + rows.iter() + .map(|row| { + let txid_blob: Vec = row.get(0); + Ok(IdoTrackerMapRpcRecord { + txid: blob_to_display_hex::(&txid_blob)?, + next_output_index: row.get(1), + height: row.get(2), + txchain_id: row.get(3), + }) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use num_bigint::BigInt; + use sqlx::SqlitePool; + + async fn make_pool() -> SqlitePool { + let pool = SqlitePool::connect("sqlite::memory:").await.unwrap(); + prepare_tables(&pool).await; + pool + } + + fn txid(n: u8) -> Vec { + vec![n; 32] + } + + async fn insert_test_ido( + pool: &SqlitePool, + preinit_txid: Vec, + status: &str, + is_valid: bool, + offered_token_id: Option>, + offering_token_id: Option>, + ) -> i64 { + sqlx::query( + "INSERT INTO ido (preinit_txid, init_txid, launch_txid, offering_token_id, + offered_token_id, status, parameters, state, is_valid, txchain_entrypoint, txchain_head) + VALUES (?, NULL, NULL, ?, ?, ?, ?, ?, ?, NULL, NULL)", + ) + .bind(preinit_txid) + .bind(offering_token_id) + .bind(offered_token_id) + .bind(status) + .bind(b"{}".as_slice()) + .bind(b"{}".as_slice()) + .bind(is_valid as i64) + .execute(pool) + .await + .unwrap() + .last_insert_rowid() + } + + // ─── vm number encoding ─────────────────────────────────────────────────── + + #[test] + fn vm_number_roundtrip_zero() { + let n = BigInt::from(0i64); + assert_eq!(bigint_to_vm_number(&n), Vec::::new()); + assert_eq!(vm_number_to_bigint(&[]), n); + } + + #[test] + fn vm_number_roundtrip_positive() { + for v in [1i64, 127, 128, 255, 256, 32767, 65535, 1_000_000] { + let n = BigInt::from(v); + assert_eq!(vm_number_to_bigint(&bigint_to_vm_number(&n)), n, "v={v}"); + } + } + + #[test] + fn vm_number_roundtrip_negative() { + for v in [-1i64, -127, -128, -255, -256, -65535] { + let n = BigInt::from(v); + assert_eq!(vm_number_to_bigint(&bigint_to_vm_number(&n)), n, "v={v}"); + } + } + + #[test] + fn vm_number_sign_bit_boundary() { + // 127 fits in one byte without extra sign byte; 128 requires one + assert_eq!(bigint_to_vm_number(&BigInt::from(127i64)).len(), 1); + assert_eq!(bigint_to_vm_number(&BigInt::from(128i64)).len(), 2); + assert_eq!(bigint_to_vm_number(&BigInt::from(-127i64)).len(), 1); + assert_eq!(bigint_to_vm_number(&BigInt::from(-128i64)).len(), 2); + } + + // ─── encode_padded_vm_number ────────────────────────────────────────────── + + #[test] + fn padded_vm_number_roundtrip() { + for (v, len) in [(0i64, 4), (1, 4), (127, 4), (255, 4), (65535, 4), (0x7FFFFFFFi64, 4)] { + let n = BigInt::from(v); + let enc = encode_padded_vm_number(&n, len).expect("encode failed"); + assert_eq!(enc.len(), len, "wrong length for {v}"); + assert_eq!(decode_padded_vm_number(&enc), n, "roundtrip failed for {v}"); + } + } + + #[test] + fn padded_vm_number_overflow_is_err() { + // value needs 5 bytes, cannot fit in 4 + let n = BigInt::from(0xFFFF_FFFFi64 + 1); + assert!(encode_padded_vm_number(&n, 4).is_err()); + } + + #[test] + fn padded_vm_number_zero_any_length() { + for len in [1usize, 2, 4, 8] { + let enc = encode_padded_vm_number(&BigInt::from(0i64), len).unwrap(); + assert_eq!(enc.len(), len); + assert_eq!(decode_padded_vm_number(&enc), BigInt::from(0i64)); + } + } + + // ─── bigint_to_push_opcode ──────────────────────────────────────────────── + + #[test] + fn push_opcode_zero() { + assert_eq!(bigint_to_push_opcode(&BigInt::from(0i64)), vec![0x00]); + } + + #[test] + fn push_opcode_minus_one() { + assert_eq!(bigint_to_push_opcode(&BigInt::from(-1i64)), vec![0x4f]); + } + + #[test] + fn push_opcode_small_range_1_to_16() { + for v in 1u8..=16 { + let result = bigint_to_push_opcode(&BigInt::from(v)); + assert_eq!(result, vec![0x50 + v], "v={v}"); + } + } + + #[test] + fn push_opcode_17_is_data_push() { + let result = bigint_to_push_opcode(&BigInt::from(17i64)); + // length prefix + one byte payload + assert_eq!(result, vec![0x01, 17]); + } + + #[test] + fn push_opcode_roundtrip_via_vm_number() { + // bigint_to_push_opcode should produce bytes whose payload decodes back + let n = BigInt::from(12345i64); + let opcode = bigint_to_push_opcode(&n); + // first byte is the length; rest is the vm-number payload + let payload = &opcode[1..]; + assert_eq!(vm_number_to_bigint(payload), n); + } + + // ─── is_preinit_state_ready_to_init ────────────────────────────────────── + + #[allow(non_snake_case)] + fn ready_state() -> IdoPreInitState { + IdoPreInitState { + authguardCategory: vec![0xAA; 32], + idoCategory: vec![0xBB; 32], + nextTxSetterFlag: BigInt::from(0i64), + oTokenGenerated: BigInt::from(1i64), + } + } + + #[test] + fn preinit_ready_when_all_conditions_met() { + assert!(is_preinit_state_ready_to_init(&ready_state())); + } + + #[test] + #[allow(non_snake_case)] + fn preinit_not_ready_o_token_not_generated() { + let mut s = ready_state(); + s.oTokenGenerated = BigInt::from(0i64); + assert!(!is_preinit_state_ready_to_init(&s)); + } + + #[test] + #[allow(non_snake_case)] + fn preinit_not_ready_next_tx_setter_flag_set() { + let mut s = ready_state(); + s.nextTxSetterFlag = BigInt::from(1i64); + assert!(!is_preinit_state_ready_to_init(&s)); + } + + #[test] + #[allow(non_snake_case)] + fn preinit_not_ready_authguard_category_zero() { + let mut s = ready_state(); + s.authguardCategory = vec![0x00; 32]; + assert!(!is_preinit_state_ready_to_init(&s)); + } + + #[test] + #[allow(non_snake_case)] + fn preinit_not_ready_ido_category_zero() { + let mut s = ready_state(); + s.idoCategory = vec![0x00; 32]; + assert!(!is_preinit_state_ready_to_init(&s)); + } + + // ─── DB: list_idos ──────────────────────────────────────────────────────── + + #[rocket::async_test] + async fn list_idos_returns_all() { + let pool = make_pool().await; + insert_test_ido(&pool, txid(1), "PREINIT", true, None, None).await; + insert_test_ido(&pool, txid(2), "ACTIVE", true, None, None).await; + let results = list_idos(&pool, None, None, None, 0, 100).await.unwrap(); + assert_eq!(results.len(), 2); + } + + #[rocket::async_test] + async fn list_idos_filter_is_valid() { + let pool = make_pool().await; + insert_test_ido(&pool, txid(1), "PREINIT", true, None, None).await; + insert_test_ido(&pool, txid(2), "PREINIT", false, None, None).await; + let valid = list_idos(&pool, Some(true), None, None, 0, 100).await.unwrap(); + assert_eq!(valid.len(), 1); + assert!(valid[0].is_valid); + let invalid = list_idos(&pool, Some(false), None, None, 0, 100).await.unwrap(); + assert_eq!(invalid.len(), 1); + assert!(!invalid[0].is_valid); + } + + #[rocket::async_test] + async fn list_idos_filter_status() { + let pool = make_pool().await; + insert_test_ido(&pool, txid(1), "PREINIT", true, None, None).await; + insert_test_ido(&pool, txid(2), "ACTIVE", true, None, None).await; + insert_test_ido(&pool, txid(3), "ACTIVE", true, None, None).await; + let active = list_idos(&pool, None, Some("ACTIVE".to_string()), None, 0, 100).await.unwrap(); + assert_eq!(active.len(), 2); + assert!(active.iter().all(|r| r.status == "ACTIVE")); + } + + #[rocket::async_test] + async fn list_idos_filter_offered_token_id() { + let pool = make_pool().await; + let token = vec![0xAB; 32]; + insert_test_ido(&pool, txid(1), "ACTIVE", true, Some(token.clone()), None).await; + insert_test_ido(&pool, txid(2), "ACTIVE", true, None, None).await; + let filtered = list_idos(&pool, None, None, Some(token), 0, 100).await.unwrap(); + assert_eq!(filtered.len(), 1); + } + + #[rocket::async_test] + async fn list_idos_pagination() { + let pool = make_pool().await; + for i in 1..=5u8 { + insert_test_ido(&pool, txid(i), "PREINIT", true, None, None).await; + } + let page1 = list_idos(&pool, None, None, None, 0, 2).await.unwrap(); + let page2 = list_idos(&pool, None, None, None, 2, 2).await.unwrap(); + let page3 = list_idos(&pool, None, None, None, 4, 2).await.unwrap(); + assert_eq!(page1.len(), 2); + assert_eq!(page2.len(), 2); + assert_eq!(page3.len(), 1); + // IDs are ascending + assert!(page1[0].id < page1[1].id); + assert!(page1[1].id < page2[0].id); + } + + // ─── DB: get_ido_by_offering_token_id ──────────────────────────────────── + + #[rocket::async_test] + async fn get_ido_by_offering_token_found() { + let pool = make_pool().await; + let tok = vec![0xCC; 32]; + insert_test_ido(&pool, txid(1), "ACTIVE", true, None, Some(tok.clone())).await; + let result = get_ido_by_offering_token_id(&pool, tok).await.unwrap(); + assert!(result.is_some()); + assert_eq!(result.unwrap().status, "ACTIVE"); + } + + #[rocket::async_test] + async fn get_ido_by_offering_token_not_found() { + let pool = make_pool().await; + let result = get_ido_by_offering_token_id(&pool, vec![0xFF; 32]).await.unwrap(); + assert!(result.is_none()); + } + + // ─── DB: list_ido_entries ───────────────────────────────────────────────── + + async fn insert_test_entry(pool: &SqlitePool, ido_id: i64, txid_val: Vec, distributed: bool) { + sqlx::query( + "INSERT INTO ido_entry (ido_id, txid, owner_nfthash, commitment, + supply_amount, demand_amount, lockup_timeval, discount, distributed) + VALUES (?, ?, ?, NULL, 0, 0, 0, 0, ?)", + ) + .bind(ido_id) + .bind(txid_val) + .bind(vec![0u8; 32]) + .bind(distributed as i64) + .execute(pool) + .await + .unwrap(); + } + + #[rocket::async_test] + async fn list_ido_entries_all() { + let pool = make_pool().await; + let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, None, None).await; + insert_test_entry(&pool, ido_id, txid(10), false).await; + insert_test_entry(&pool, ido_id, txid(11), true).await; + let all = list_ido_entries(&pool, ido_id, None, 0, 100).await.unwrap(); + assert_eq!(all.len(), 2); + } + + #[rocket::async_test] + async fn list_ido_entries_filter_distributed() { + let pool = make_pool().await; + let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, None, None).await; + insert_test_entry(&pool, ido_id, txid(10), false).await; + insert_test_entry(&pool, ido_id, txid(11), true).await; + insert_test_entry(&pool, ido_id, txid(12), true).await; + let dist = list_ido_entries(&pool, ido_id, Some(true), 0, 100).await.unwrap(); + assert_eq!(dist.len(), 2); + assert!(dist.iter().all(|e| e.distributed)); + let undist = list_ido_entries(&pool, ido_id, Some(false), 0, 100).await.unwrap(); + assert_eq!(undist.len(), 1); + assert!(!undist[0].distributed); + } + + #[rocket::async_test] + async fn list_ido_entries_scoped_to_ido() { + let pool = make_pool().await; + let ido1 = insert_test_ido(&pool, txid(1), "ACTIVE", true, None, None).await; + let ido2 = insert_test_ido(&pool, txid(2), "ACTIVE", true, None, None).await; + insert_test_entry(&pool, ido1, txid(10), false).await; + insert_test_entry(&pool, ido1, txid(11), false).await; + insert_test_entry(&pool, ido2, txid(12), false).await; + let e1 = list_ido_entries(&pool, ido1, None, 0, 100).await.unwrap(); + let e2 = list_ido_entries(&pool, ido2, None, 0, 100).await.unwrap(); + assert_eq!(e1.len(), 2); + assert_eq!(e2.len(), 1); + } + + // ─── DB: list_ido_txchain ───────────────────────────────────────────────── + + async fn insert_txchain_item( + pool: &SqlitePool, + ido_id: i64, + txid_val: Vec, + prev_id: Option, + ) -> i64 { + sqlx::query( + "INSERT INTO ido_txchain (prev_id, ido_id, txid, tx) VALUES (?, ?, ?, ?)", + ) + .bind(prev_id) + .bind(ido_id) + .bind(txid_val) + .bind(vec![0u8; 4]) // dummy tx bytes + .execute(pool) + .await + .unwrap() + .last_insert_rowid() + } + + async fn set_txchain_head(pool: &SqlitePool, ido_id: i64, head_id: i64) { + sqlx::query("UPDATE ido SET txchain_head = ? WHERE id = ?") + .bind(head_id) + .bind(ido_id) + .execute(pool) + .await + .unwrap(); + } + + #[rocket::async_test] + async fn list_ido_txchain_ordered_oldest_first() { + let pool = make_pool().await; + let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, None, None).await; + let id_a = insert_txchain_item(&pool, ido_id, txid(10), None).await; + let id_b = insert_txchain_item(&pool, ido_id, txid(11), Some(id_a)).await; + let id_c = insert_txchain_item(&pool, ido_id, txid(12), Some(id_b)).await; + set_txchain_head(&pool, ido_id, id_c).await; + + let chain = list_ido_txchain(&pool, ido_id, 0, 100).await.unwrap(); + assert_eq!(chain.len(), 3); + assert_eq!(chain[0].id, id_a); + assert_eq!(chain[1].id, id_b); + assert_eq!(chain[2].id, id_c); + } + + #[rocket::async_test] + async fn list_ido_txchain_pagination() { + let pool = make_pool().await; + let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, None, None).await; + let id_a = insert_txchain_item(&pool, ido_id, txid(10), None).await; + let id_b = insert_txchain_item(&pool, ido_id, txid(11), Some(id_a)).await; + let id_c = insert_txchain_item(&pool, ido_id, txid(12), Some(id_b)).await; + let id_d = insert_txchain_item(&pool, ido_id, txid(13), Some(id_c)).await; + set_txchain_head(&pool, ido_id, id_d).await; + + let page1 = list_ido_txchain(&pool, ido_id, 0, 2).await.unwrap(); + let page2 = list_ido_txchain(&pool, ido_id, 2, 2).await.unwrap(); + assert_eq!(page1.len(), 2); + assert_eq!(page2.len(), 2); + assert_eq!(page1[0].id, id_a); + assert_eq!(page1[1].id, id_b); + assert_eq!(page2[0].id, id_c); + assert_eq!(page2[1].id, id_d); + } + + #[rocket::async_test] + async fn list_ido_txchain_no_head_returns_empty() { + let pool = make_pool().await; + let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, None, None).await; + // txchain_head is NULL (default from insert_test_ido) + let chain = list_ido_txchain(&pool, ido_id, 0, 100).await.unwrap(); + assert_eq!(chain.len(), 0); + } + + #[rocket::async_test] + async fn list_ido_txchain_unknown_ido_returns_empty() { + let pool = make_pool().await; + let chain = list_ido_txchain(&pool, 9999, 0, 100).await.unwrap(); + assert_eq!(chain.len(), 0); + } } diff --git a/src/db/init.rs b/src/db/init.rs index 448ba7a..73fcb7e 100644 --- a/src/db/init.rs +++ b/src/db/init.rs @@ -141,7 +141,7 @@ pub async fn initialize_databases(network: &str, read_slots: ReadSlots) -> Resul let (db_exists, ido_db_write, ido_db_read) = create_db_pool(&db_path(db_dir, "ido.db"), read_slots.ido).await; if !db_exists { - cauldron_prepare_tables(&ido_db_write).await; + ido_prepare_tables(&ido_db_write).await; } else { check_db_version(&ido_db_read).await?; } diff --git a/src/db/mod.rs b/src/db/mod.rs index f247fc3..352b173 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -9,6 +9,7 @@ pub mod bcmr; pub mod blob; pub mod cauldron; pub mod crc20; +pub mod ido; pub mod init; pub mod moria; pub mod oracle; diff --git a/src/index.rs b/src/index.rs index ffa8cbe..47d5dd8 100644 --- a/src/index.rs +++ b/src/index.rs @@ -445,6 +445,7 @@ pub async fn index_blocks( .await?; db::ido::index_block( + network, &db.ido_w, &sorted_txs, &blockhash, diff --git a/src/main.rs b/src/main.rs index c80f718..a1196f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -566,6 +566,22 @@ async fn launch() -> _ { }); } + // Always-on IDO endpoints. + let mut ido_routes = routes![ + rpc::ido::list_idos, + rpc::ido::get_ido_by_id, + rpc::ido::get_ido_by_offering_token, + rpc::ido::list_ido_entries, + rpc::ido::get_txchain_tx, + ]; + // Debug-only IDO endpoints, mounted only when `debug = true` in the config. + if debug_endpoints { + ido_routes.extend(routes![ + rpc::ido::list_ido_txchain, + rpc::ido::list_txchain_tracker_map, + ]); + } + rocket::build() .attach(signal::ShutdownFairing) .attach(signal::IbdCheckFairing) @@ -635,6 +651,7 @@ async fn launch() -> _ { rpc::tokentoken::list_tokens, ], ) + .mount("/ido", ido_routes) .mount("/", routes![rpc::health::health]) .attach(cors) } diff --git a/src/rpc/ido.rs b/src/rpc/ido.rs new file mode 100644 index 0000000..170e878 --- /dev/null +++ b/src/rpc/ido.rs @@ -0,0 +1,157 @@ +// Copyright (C) 2024-2026 Whiterun LLC +// +// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later. +// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html + +use crate::db::blob::display_hex_to_blob; +use crate::db::DB; +use crate::rpc::err::{bad_request, db_error, ApiErrorCode, CachedApiResult}; +use crate::rpc::response::{cached_ok, CACHE_NONE}; +use bitcoincash::TokenID; +use rocket::{get, State}; +use serde_json::Value; + +const LIST_DEFAULT_LIMIT: i64 = 20; +const LIST_MAX_LIMIT: i64 = 100; + +const DEBUG_DEFAULT_LIMIT: i64 = 100; +const DEBUG_MAX_LIMIT: i64 = 10_000; + +/// List IDOs with optional filters and pagination. +/// +/// Filters: +/// - `is_valid`: boolean filter on the is_valid flag +/// - `status`: one of PREINIT, ACTIVE, DISTRIBUTING, DISTRIBUTED +/// - `offered_token_id`: 64-char hex token ID (reversed display format) +/// +/// Pagination: `offset` (default 0), `limit` (default 20, max 100) +#[get("/list?&&&&")] +pub async fn list_idos( + is_valid: Option, + status: Option, + offered_token_id: Option<&str>, + offset: Option, + limit: Option, + db: &State, +) -> CachedApiResult { + let offset = offset.unwrap_or(0).max(0); + let limit = limit.unwrap_or(LIST_DEFAULT_LIMIT).clamp(1, LIST_MAX_LIMIT); + + let offered_token_id_blob = offered_token_id + .map(|s| { + display_hex_to_blob::(s).map_err(|e| { + bad_request( + ApiErrorCode::InvalidParameters, + &format!("Invalid offered_token_id: {e}"), + ) + }) + }) + .transpose()?; + + let items = + crate::db::ido::list_idos(&db.ido_r, is_valid, status, offered_token_id_blob, offset, limit) + .await + .map_err(db_error)?; + + Ok(cached_ok(serde_json::to_value(items).unwrap(), CACHE_NONE)) +} + +/// Get a single IDO by its offering token ID. Returns null if not found. +#[get("/by-offering-token/")] +pub async fn get_ido_by_offering_token( + offering_token_id: &str, + db: &State, +) -> CachedApiResult { + let blob = display_hex_to_blob::(offering_token_id).map_err(|e| { + bad_request( + ApiErrorCode::InvalidParameters, + &format!("Invalid offering_token_id: {e}"), + ) + })?; + + let item = crate::db::ido::get_ido_by_offering_token_id(&db.ido_r, blob) + .await + .map_err(db_error)?; + + Ok(cached_ok(serde_json::to_value(item).unwrap(), CACHE_NONE)) +} + +/// List entries for an IDO. +/// +/// - `ido_id`: the integer IDO ID from the ido table +/// - `distributed`: optional boolean filter +/// +/// Pagination: `offset` (default 0), `limit` (default 20, max 100) +#[get("//entries?&&")] +pub async fn list_ido_entries( + ido_id: i64, + distributed: Option, + offset: Option, + limit: Option, + db: &State, +) -> CachedApiResult { + let offset = offset.unwrap_or(0).max(0); + let limit = limit.unwrap_or(LIST_DEFAULT_LIMIT).clamp(1, LIST_MAX_LIMIT); + + let items = crate::db::ido::list_ido_entries(&db.ido_r, ido_id, distributed, offset, limit) + .await + .map_err(db_error)?; + + Ok(cached_ok(serde_json::to_value(items).unwrap(), CACHE_NONE)) +} + +/// [Debug] List the txchain for an IDO, sorted oldest-first (head is last). +/// +/// Pagination: `offset` (default 0), `limit` (default 100, max 10000) +#[get("//txchain?&")] +pub async fn list_ido_txchain( + ido_id: i64, + offset: Option, + limit: Option, + db: &State, +) -> CachedApiResult { + let offset = offset.unwrap_or(0).max(0); + let limit = limit.unwrap_or(DEBUG_DEFAULT_LIMIT).clamp(1, DEBUG_MAX_LIMIT); + + let items = crate::db::ido::list_ido_txchain(&db.ido_r, ido_id, offset, limit) + .await + .map_err(db_error)?; + + Ok(cached_ok(serde_json::to_value(items).unwrap(), CACHE_NONE)) +} + +/// [Debug] Get the raw transaction hex for a txchain item by its ID. +/// Returns `{"tx": ""}` or `{"tx": null}` if not found. +#[get("/txchain//tx")] +pub async fn get_txchain_tx( + txchain_item_id: i64, + db: &State, +) -> CachedApiResult { + let tx_hex = crate::db::ido::get_txchain_tx_hex(&db.ido_r, txchain_item_id) + .await + .map_err(db_error)?; + + Ok(cached_ok( + serde_json::json!({ "tx": tx_hex }), + CACHE_NONE, + )) +} + +/// [Debug] List all entries in the txchain tracker map. +/// +/// Pagination: `offset` (default 0), `limit` (default 100, max 10000) +#[get("/txchain-tracker?&")] +pub async fn list_txchain_tracker_map( + offset: Option, + limit: Option, + db: &State, +) -> CachedApiResult { + let offset = offset.unwrap_or(0).max(0); + let limit = limit.unwrap_or(DEBUG_DEFAULT_LIMIT).clamp(1, DEBUG_MAX_LIMIT); + + let items = crate::db::ido::list_txchain_tracker_map(&db.ido_r, offset, limit) + .await + .map_err(db_error)?; + + Ok(cached_ok(serde_json::to_value(items).unwrap(), CACHE_NONE)) +} diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 52d186f..364f589 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -17,6 +17,7 @@ pub mod candlesticks; pub mod contract; pub mod err; pub mod health; +pub mod ido; pub mod moria; pub mod oracle; pub mod pool; diff --git a/src/utiltest.rs b/src/utiltest.rs index 1545de0..c330ec5 100644 --- a/src/utiltest.rs +++ b/src/utiltest.rs @@ -43,6 +43,8 @@ mod test_utils { oracle_r: pool.clone(), moria_w: pool.clone(), moria_r: pool.clone(), + ido_w: pool.clone(), + ido_r: pool.clone(), } } }