#![allow(non_snake_case)] use anyhow::Result; use log::{info,warn}; use num_bigint::{BigInt, Sign}; use num_traits::{Zero, ToPrimitive}; use bitcoincash::blockdata::script::{Builder, Script, Instruction}; use bitcoincash::blockdata::opcodes; use bitcoin_hashes::sha256d; //NOTUSED //use bitcoin_hashes::hash160; use bitcoin_hashes::Hash; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; 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}; use anyhow::Error; const BCMR_SIGNATURE: &[u8] = &[ 0x6a, 0x04, 0x42, 0x43, 0x4d, 0x52, 0x20, ]; 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; 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 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% static MIN_PLP_AFTER_DISCOUNT: LazyLock = LazyLock::new(|| BigInt::from(5_000_000i64)); // 5% 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 CHIPNET_DELPHI_TOKEN_ID: &[u8; 32] = &[ 0x1b, 0x94, 0x82, 0x40, 0xc9, 0xcf, 0xaa, 0x82, 0x24, 0x38, 0x3d, 0xb1, 0x6e, 0x47, 0x35, 0xef, 0xa0, 0x9c, 0x94, 0x26, 0xd1, 0x71, 0x3d, 0x26, 0x5e, 0x84, 0x9a, 0x8e, 0x91, 0xf2, 0x66, 0x9a ]; const MAINNET_DELPHI_TOKEN_ID: &[u8; 32] = &[ 0xbe, 0x0d, 0x0d, 0x83, 0x24, 0xe8, 0xcd, 0xa4, 0x1d, 0x34, 0xb8, 0x5b, 0xd2, 0x03, 0xce, 0x24, 0x82, 0x25, 0x6e, 0xb3, 0x37, 0xa0, 0xad, 0x0f, 0xea, 0x82, 0xc2, 0xdd, 0xd7, 0x30, 0x6c, 0x88 ]; // TODO:: set the platform fee nfth 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, ]; const IDO_SIGNATURE: &[u8] = &[ 0x12, 0x43, 0x61, 0x75, 0x6c, 0x64, 0x72, 0x6f, 0x6e, 0x49, 0x64, 0x6f, 0x2d, 0x32, 0x30, 0x32, 0x36, 0x51, 0x32, 0x75, ]; 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("c0009d00c852c88852c9529d00c854c88854c9549d00c855c88855c9559d00ce01207f7551ce78527e8851cf517f755f8401008853ce01207f7555798853cf567f75817600a06953d100876453d101207f7552798791696851d0547aa07651d0557aa09b63537952799f696851cf557f77547f758100a16302aa2001205d797e57797eaa7e01877e556085537956807e0058807e52d058807e0058807e00d28800d154798800d3009d00cd788851cd788851cc52c6a26951d152ce8851d352d09d51d2008852d10088c4549d75675279517e00d18800d3009d766355ca827755ca7853947f77527f758155ca527953945279947f77787f75526085557956807e51cf557f77547f757e0058807e52d058807e0058807e00d28802aa20c101147f755a79827751807e5a797e01207e60797e52797eaa7e01877e00cd8802aa20012060797e5a797eaa7e01877e52cd8852cc52c6a26952d152ce8852d352d09d52d2008854d10088c4559d6d756754ca827754ca7853947f77527f758154ca527953945279947f77787f7551d07600a06354d152ce8854d3789d54d2008802aa2001200111797e5b797eaa7e01877e54cd8855d10088c4569d6754d10088c4559d6852567956807e51cf557f77547f757e0058807e7858807e0058807e00d28802aa20c101147f755d79827751807e5d797e5c79827751807e5c797e5b79827751807e5b797e01207e5a797e547e5f797e01207e60797e01207e0111797e53797eaa7e01877e00cd8852d152ce8852d352d05279949d52cc52c6a26902aa20520052807e5d797eaa7e01877e52cd8852d200886d6d6802aa2056798277518057797e5a797eaa7e01877e51cd88545b797e51d28851cc51c6a26951d153798851d3009d686d6d6d6d6d6d51").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("c0c9009dc0c85b79c8885a79c9577a9d5979ce827701209d567900a263c0c85b79c8885a79c957799d68c0c800d1788800d2578800d3009d00cd5a7a88c08bc0c878c88876c9547a9d76ca827778ca7853947f77527f75817bca7b53945279947f777c7f75c05293c0c878c88876c9557a9d76ca827778ca7853947f77527f75817bca7b53945279947f777c7f75c05393c0c878c88876c9567a9d76ca827778ca7853947f77527f75815178529302ff00a063755367785293014ba063755268685379ca537a5394537a947b947f7702aa20525352807e5b797e7b7eaa7e01877e54cd8854cc7cc6a26954d10088c05493c0c878c88876c9567a9d76ca827778ca7853947f77527f75815178529302ff00a063755367785293014ba063755268685379ca537a5394537a947b947f7702aa20525352807e5a797e7b7eaa7e01877e55cd8855cc7cc6a26955d100885779d07600a0690100557a7e04000000007e51d28851d15479527e8851d3789d51cd02aa20547aaa7e01877e885153d28853d153798853d3009d53cd02aa20537aaa7e01877e8802aa20525352807e567a7eaa7e01877e52cd8852cc5579c6a26952d1557ace8852d39d52d20088c05593c0c878c88876c9537a9d76c7827778c77853947f77527f75817bc77b53945279947f777c7f7576827700a06376517f75768176014b9f788b547982779f9a635279788b7f77537a757c6b7c6c5279517f757b757c6878016a8764016a537a757c6b7c6c686d67016a776856cd8856d1008857d100876457d101207f75788791696857cd567f75066a0442434d52879169c4589e6358d100876458d101207f75788791696858cd567f75066a0442434d52879169c4599e6359d100876459d101207f75788791696859cd567f75066a0442434d52879169c45a9e635ad10087645ad101207f7578879169685acd567f75066a0442434d52879169c45b9e635bd10087645bd101207f7578879169685bcd567f75066a0442434d52879169c45c9d686868686d7551").unwrap()); static OFFERING_INTEGRATED_TIMELOCKED_P2NFTH_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("c0cf527f7701207f75c0cf01227f77815479ce01207f757b88537acf567f75817600a0699f6978ce7bcf7eaa88c0cf517f77517f7581c0c85279c8887cc99c").unwrap()); static IDO_INITIATOR_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("c0ce827701209dc0cf827700a0695579827701209dc0c85779c8885679c99dc0c85779c8885679c99d5479529376ca827778ca7853947f77527f75817bca7b53945279947f777c7f7501157f7701207f75c0cec0cf7eaa88547ac852d1827701209d52d300a06902aa20c101147f755479827751807e54797e5379827751807e537a7e01207e7b7e01207e52d17e01207e547a7e587e52d358807e537a7eaa7e01877e57cd8857ccc0c6a26957d1c0ce8857d2c0cf8857d3c0d09d02aa20525752807e7b7eaa7e01877e7658cd8858cc02e803a26958d1008859cd8859cc78c6a26959d178ce8859d278cf8859d37cd09c").unwrap()); static IDO_POSTLAUNCH_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("5879009c63c0009dc0c9009e69c0cdc0c788c0ccc0c6a269c0d1c0ce88c0d3c0d09dc0d2c0cf8802aa20520052807e597a7eaa7e01877ec0c851c88851c9589d7651cd8851cc51c6a26951d151ce8851d351d09dc0c852c88852c9599d52cd8852cc52c6a26952d152ce8852d352d09dc353a06353ce0088c3549d6853d10088c4549d6d6d6d6d51675879519c63c0009dc0c9009dc0cdc0c788c0d1c0ce88c0d3c0d09dc0d2c0cf88c0c852c88852c9529d52cd52c78852cc52c6a26952d152ce8852d352d09d02aa200120c0cec0cf7eaa7e587a7eaa7e01877e00005376c75479876376ce59798763527978d093537a757c6b7c6c6776ce0088686ec6937b757c6776ce008868768bc378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c3789d6868686868686868c0ccc0c6547a93a269c0c851c88851c9519d51cd51c78851cc51c6a26951d0537a937600a06351d159798851d378a26951d200886751d100886853d10088c4549d6d6d6d6d6d6d5167587a529dc0009dc0c9009d5379827701209d53ce567a8853cf517f755f845588c0c653cf577f77587f7581a269c0c851c88851c9519d53cf5f7f77587f75817600a06351d078a2696851d000a06351ce5679886851cf0088c0c852c88852c9529d52ce567988000053cf517f75608401008763557981537994765679950400e1f505967652d0a06352d07768765679950500e87648179653cf01177f77587f758194760500e8764817955779967802e803a27800a09a6378567a757c6b7c6b7c6b7c6b7c6c6c6c6c765379a16376557a757c6b7c6b7c6b7c6c6c6c675279557a757c6b7c6b7c6b7c6c6c6c68686d6d68c0c651c69352c69352799451d052d093527994537900a0537900a09a6302aa2005746376a914000114807e2b88ac67c0d1c0ce88c25288c0cdc0c788c0c6c0d095c0c6c0cc9490539502e80396c0cc7c94c0d3957ca2687eaa7e01877e00cd8800cc54799d00d15a798800d353799d00d20088096a0653554d4d4f4e14000114807e51cd8851cc009d51d100886700d1008800cd016a8851d1008851cd016a886802aa2001205a7a7e5b7a7eaa7e01877e52cd8852cc7ba2697600a06352d378a26952d158798852d200886752d100886853d10088c4549d6d6d6d6d75516868").unwrap()); static IDO_PREINIT_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("c0519dc0c800c88800c9529dc0c852c88852c9539dc0c853c88853c9549dc0c854c88854c9559dc0c855c88855c9569dc0c856c88856c9579d5c79009e63c0c858c88858c9599d67c0c858c88858c9599d68c0c857c88857c9589d597981009c5d79009c9b5b7981009c9b63c0d1008852cd00c78852d1008853cd52c78853d1008854cd53c78854d1008855cd54c78855d1008856cd55c78856d1008857cd56c78857d100885c79009e6359cd58c78859cc58c6a26959d158ce8859d358d09d59d258cf8867597981009e5c79009e9a6459cd58c78859d10088686858cd57c78858d1008802aa200302010055797eaa7e01877ec101587f775b7981009c6301005f79009e63015177685e79009c6300cd53798800d10088760251207e5e797e01207e5d797e52797e7b757c67c0c859c88859c9009d00cd016a8800d100885acd5c79885ad1c0c8885ad3009d5ad20100885bd10088c45c9d760200207e5e797e01207ec0c87e52797e7b757c6875675e79009c635d79009c6300cd52798800d10088030051205d797e01207e5c797e787e7767c0c859c88859c9009d53795579950400e1f50596547978935479789455795279a06902aa20012060797e5d797e5c797eaa7e01877e5c798277009c6302a91401200111797e5c797ea97e01877e776800cd788800d1c0c88800d352799d00d2008859cd56798859d1c0c88859d353799d59d2008858c7827758c77853947f77527f758158c7527953945279947f77787f7576827700a06376517f75768176014b9f788b547982779f9a635279788b7f77537a757c6b7c6c5279517f757b757c6878016a8764016a537a757c6b7c6c686d67016a77685acd78885ad100885bd10088c45c9d035100200114797e01207e0113797e58797e587a757c6b7c6b7c6b7c6b7c6b7c6b7c6c6c6c6c6c6c6d6d6d7568675d79009c6300cd52798800d10088035151205d797e01207e5c797e787e7767c0c859c88859c9009d00cd016a8800d100885acd5279885ad1c0c8885ad3009d5ad201ff885bd10088c45c9d03510020c0c87e01207e5c797e787e7768686802aa20c101147f7552797eaa7e01877ec0cd886d67c0c859c88859c95a9d55ca827755ca7853947f77527f758155ca527953945279947f77787f75012302aa2001205f797e5d797e5b797eaa7e01877e7e5b798277009c63011702a914012060797e5b797ea97e01877e7e776800cd02aa20c101147f7553797e54797eaa7e01877e8800d1008800ca827700ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686800ca537953945379945279947f7702aa20030200005d797e52797eaa7e01877e51cd8851d1008852ca827752ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686852ca537953945379945279947f7702aa20030200000111797e707c0114937f757e01207e01187901ff7eaa7e707c0135937f777eaa7e01877e52cd8852d1008853ca827753ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686853ca537953945379945279947f7702aa20030200000115797e52797eaa7e01877e53cd8853d1008854ca827754ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686854ca537953945379945279947f7702aa20030200000119797e52797eaa7e01877e54cd8854d1008857c7827757c77853947f77527f75815178529302ff00a063755367785293014ba0637552686857c7537953945379945279947f7755cd03020000011d797e52797e8855d1008802aa2003020000011d797eaa7e01877e56cd8856cc58c6a26956d158ce8856d3011a799d56d2008856ca827756ca7853947f77527f758156ca527953945279947f77787f7557cd02aa20c101147f7501207e01277901007eaa7e53797eaa7e01877e8857cc59c6a26957d159ce8857d359d09d57d259cf8802aa20525752807e0120797eaa7e01877e58cd8858d158ce8858d358d0011e79949d58d200886d6d6d6d6d6d6d6d6d6d6d6d6d75686d6d6d6d6d6d7551").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()); static REBUILD_IPFS_PLACEHOLDER_BCMR_FOR_GENESIS_WITH_AUTHGUARD_CONTRACT: LazyLock> = LazyLock::new(|| hex::decode("0902a9147ca97e01877e57897c6b00c08851d100887c635ab2756d51cd016a88674d0301404142434445464748494a4b4c4d4e4f505152535455565758595a6162636465666768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f6b007c8253a26365537f7c76011299527f77816c766b7c7f77517f75785c99527f77013f84816c766b7c7f77517f757e785699527f77013f84816c766b7c7f77517f757e7c527f77013f84816c766b7c7f77517f757e7b7c7e7c82539f666882760087636d677d537c94007c807e76011299527f77816c766b7c7f77517f75785c99527f77013f84816c766b7c7f77517f757e785699527f77013f84816c766b7c7f77517f757e7c527f77013f84816c766b7c7f77517f757e7c51937f757e686c7554893a10303132333435363738396162636465667c006b65517f7c5279785f84817f77517f756c7e6b52797c5499817f77517f756c7e6b82009c666d6c5689097f7b827b7c7f777e7e538978a8886b518ac0ce568a65766c537a538a6b74519c66756c766ba804015512207c7e548a01757c7e6b528a6c7c6b65766c537a538a6b74519c66756c6ca87c7e076a0442434d52207c7e51cd886801206c0f51ce8851d0009d6300cdc0c78868517e7e578a00cd8800ce00d18800d2008800d000d38800c600cca26952d10088c45387").unwrap()); #[serde_as] #[derive(Serialize, Deserialize, Clone)] pub struct IpfsBcmrWithPlaceholderMetadata { #[serde_as(as = "serde_with::hex::Hex")] registryReplaceCalls: Vec, #[serde_as(as = "serde_with::hex::Hex")] bcmrUrisReplaceCalls: Vec, } #[serde_as] #[derive(Serialize, Deserialize, Clone)] pub struct IpfsBcmrWithPlaceholder { metadata: IpfsBcmrWithPlaceholderMetadata, #[serde_as(as = "serde_with::hex::Hex")] contentHash: Vec, #[serde_as(as = "serde_with::hex::Hex")] urisBytecode: Vec, } #[derive(Serialize, Deserialize, Clone)] pub struct IdoPreInitBcmrParameters { oToken: Option, offering: Option, } pub struct IdoPreInitLockParameters { preInitBcmr: IdoPreInitBcmrParameters, authguardLockingBytecode: Vec, permanentLiquidityShareNumerator: BigInt, offeredTokenAmount: BigInt, offeredTokenTotalSupply: BigInt, } #[serde_as] #[derive(Serialize, Deserialize, Clone)] pub struct IdoParametersOfferingLaunchConditions { #[serde_as(as = "DisplayFromStr")] expiresAt: BigInt, #[serde_as(as = "DisplayFromStr")] deployThreshold: BigInt, #[serde_as(as = "DisplayFromStr")] immediateDeployThreshold: BigInt, } #[serde_as] #[derive(Serialize, Deserialize, Clone)] pub struct IdoParametersOfferingOffer { #[serde_as(as = "DisplayFromStr")] maxDiscountRateNumerator: BigInt, #[serde_as(as = "DisplayFromStr")] discountAnnualRateNumerator: BigInt, #[serde_as(as = "DisplayFromStr")] priceNumerator: BigInt, #[serde_as(as = "DisplayFromStr")] minOffer: BigInt, #[serde_as(as = "Option")] xTokenCategory: Option>, } #[serde_as] #[derive(Serialize, Deserialize, Clone)] pub struct IdoParametersOffering { #[serde_as(as = "DisplayFromStr")] platformFeeNumerator: BigInt, #[serde_as(as = "serde_with::hex::Hex")] platformFeeNFTH: Vec, #[serde_as(as = "serde_with::hex::Hex")] delphiCategory: Vec, launchConditions: IdoParametersOfferingLaunchConditions, offer: IdoParametersOfferingOffer, #[serde_as(as = "DisplayFromStr")] executionFee: BigInt, } #[serde_as] #[derive(Serialize, Deserialize, Clone)] pub struct IdoPreInitParameters { preInitBcmr: IdoPreInitBcmrParameters, #[serde_as(as = "serde_with::hex::Hex")] authguardLockingBytecode: Vec, #[serde_as(as = "DisplayFromStr")] authguardNftOutputIndex: BigInt, #[serde_as(as = "DisplayFromStr")] permanentLiquidityShareNumerator: BigInt, #[serde_as(as = "DisplayFromStr")] offeredTokenTotalSupply: BigInt, #[serde_as(as = "DisplayFromStr")] offeredTokenAmount: BigInt, offering: IdoParametersOffering, } #[serde_as] #[derive(Serialize, Deserialize, Clone)] pub struct IdoActiveParameters { preInitBcmr: IdoPreInitBcmrParameters, #[serde_as(as = "serde_with::hex::Hex")] collectorNFTH: Vec, #[serde_as(as = "DisplayFromStr")] permanentLiquidityShareNumerator: BigInt, #[serde_as(as = "DisplayFromStr")] offeredTokenAmount: BigInt, offering: IdoParametersOffering, } #[derive(Serialize, Deserialize, Clone)] #[serde(tag = "type")] pub enum IdoParameters { PreInit(IdoPreInitParameters), Active(IdoActiveParameters), } #[serde_as] #[derive(Serialize, Deserialize, Clone)] pub struct IdoPreInitState { #[serde_as(as = "serde_with::hex::Hex")] authguardCategory: Vec, #[serde_as(as = "serde_with::hex::Hex")] idoCategory: Vec, #[serde_as(as = "DisplayFromStr")] nextTxSetterFlag: BigInt, #[serde_as(as = "DisplayFromStr")] oTokenGenerated: BigInt, } #[serde_as] #[derive(Serialize, Deserialize, Clone)] pub struct IdoActiveState { #[serde_as(as = "DisplayFromStr")] counter: BigInt, } #[serde_as] #[derive(Serialize, Deserialize, Clone)] pub struct IdoDistributingState { isRefund: bool, #[serde_as(as = "DisplayFromStr")] timestamp: BigInt, #[serde_as(as = "DisplayFromStr")] counter: BigInt, #[serde_as(as = "DisplayFromStr")] earnedAmount: BigInt, #[serde_as(as = "DisplayFromStr")] refundAmount: BigInt, #[serde_as(as = "DisplayFromStr")] discountAmount: BigInt, #[serde_as(as = "DisplayFromStr")] platformEarnedAmount: BigInt, } #[serde_as] #[derive(Serialize, Deserialize, Clone)] pub struct IdoPostLaunchState { isRefund: bool, #[serde_as(as = "DisplayFromStr")] timestamp: BigInt, #[serde_as(as = "DisplayFromStr")] earnedAmount: BigInt, #[serde_as(as = "DisplayFromStr")] refundAmount: BigInt, #[serde_as(as = "DisplayFromStr")] discountAmount: BigInt, #[serde_as(as = "DisplayFromStr")] platformEarnedAmount: BigInt, } #[serde_as] #[derive(Serialize, Deserialize, Clone)] pub struct IdoPermanentPoolV0 { #[serde_as(as = "DisplayFromStr")] tokenAmount: BigInt, #[serde_as(as = "DisplayFromStr")] satoshiAmount: BigInt, } #[serde_as] #[derive(Serialize, Deserialize, Clone)] pub struct IdoDistributedState { permanentPool: Option, #[serde_as(as = "DisplayFromStr")] refundAmount: BigInt, #[serde_as(as = "DisplayFromStr")] discountAmount: BigInt, #[serde_as(as = "DisplayFromStr")] collectorEarnedAmount: BigInt, #[serde_as(as = "DisplayFromStr")] platformEarnedAmount: BigInt, } #[derive(Serialize, Deserialize, Clone)] #[serde(tag = "type")] pub enum IdoState { PreInit(IdoPreInitState), Active(IdoActiveState), Distributing(IdoDistributingState), PostLaunch(IdoPostLaunchState), Distributed(IdoDistributedState), } /*UNUSED fn script_concat(script: Script, v: &[u8]) -> Script { 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_storage_script(governingOutpointIndex: u32) -> Script { let mut bytes = Builder::new() .push_slice(&encode_padded_vm_number(&BigInt::from(governingOutpointIndex), 2).unwrap()) .into_script() .to_bytes(); bytes.extend_from_slice(&STORAGE_CONTRACT); Script::from(bytes) } fn build_storage_script_with_data_and_size(governingOutpointIndex: u32, data: &[u8]) -> Script { let mut data_and_size = Vec::new(); data_and_size.extend_from_slice(data); data_and_size.extend_from_slice(&encode_padded_vm_number(&BigInt::from(data.len()), 2).unwrap()); let mut bytes = Builder::new() .push_slice(&encode_padded_vm_number(&BigInt::from(governingOutpointIndex), 2).unwrap()) .into_script() .to_bytes(); bytes.extend_from_slice(&STORAGE_CONTRACT); let suffix = Builder::new() .push_slice(&data_and_size) .push_opcode(opcodes::all::OP_DROP) .into_script() .to_bytes(); bytes.extend_from_slice(&suffix); Script::from(bytes) } fn extract_data_from_storage_script_with_data_and_size(script: &Script) -> Result> { let inst_list: Vec<_> = script.instructions().collect(); let n = inst_list.len(); if n < 2 { return Err(anyhow::anyhow!("storage with data should have at least two instructions")); } match &inst_list[n - 2] { Ok(Instruction::PushBytes(data)) => Ok(data[0..data.len()-2].to_vec()), _ => Err(anyhow::anyhow!("storage with data should have a push opcode at instructions[count - 2]")), } } fn extract_data_from_unlocking_bytecode_of_storage_with_data_and_size(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)) => { extract_data_from_storage_script_with_data_and_size(&Script::from(redeem_bytecode.to_vec())) }, _ => Err(anyhow::anyhow!("invalid unlocking bytecode, p2sh redeem script push opcode is expected")), } } 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 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 rev_delphi_cat: Vec = delphiCategory.iter().copied().rev().collect(); let mut input_bytecode = Builder::new() .push_slice(collectorNFTH) .push_slice(platformFeeNFTH) .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(&rev_delphi_cat) .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_offering_initiator_bytecode(extensionOutpointIndex: &BigInt, tokenStorageOutpointIndex: &BigInt, offeringBcmrStorageOutpointIndex: &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(offeringBcmrStorageOutpointIndex)); 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.push(0x04); // OP_PUSH4 bytecode.extend_from_slice(&encode_padded_vm_number(executionFee, 4).unwrap()); bytecode.extend_from_slice(&OFFERING_INITIATOR_CONTRACT); bytecode } 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_initiator_bytecode(postLaunchPartialBytecode: &[u8], permanentPoolOTokenReserveOutpointIndex: &BigInt, offeringInitiatorOutpointIndex: &BigInt) -> Vec { let mut bytecode = Builder::new() .push_slice(postLaunchPartialBytecode) .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 create_rebuild_ipfs_placeholder_bcmr_partial_inputs(data: &IpfsBcmrWithPlaceholder) -> Vec { let mut bcmrUrisWithReplaceCalls = Vec::new(); bcmrUrisWithReplaceCalls.extend_from_slice(&data.metadata.bcmrUrisReplaceCalls); bcmrUrisWithReplaceCalls.extend_from_slice( &Builder::new() .push_slice(&data.urisBytecode) .into_script() .to_bytes() ); Builder::new() .push_slice(&data.contentHash) .push_slice(&data.metadata.registryReplaceCalls) .push_int(1i64) // OP_DEFINE .push_opcode(bitcoincash::blockdata::opcodes::All::from(0x89u8)) .push_slice(&bcmrUrisWithReplaceCalls) .push_int(2i64) // OP_DEFINE .push_opcode(bitcoincash::blockdata::opcodes::All::from(0x89u8)) .into_script() .to_bytes() } 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 offering_bcmr_partial_inputs = match ¶meters.preInitBcmr.offering { Some(bcmr) => create_rebuild_ipfs_placeholder_bcmr_partial_inputs(bcmr), _ => Vec::new(), }; let oToken_bcmr_partial_inputs = match ¶meters.preInitBcmr.oToken { Some(bcmr) => create_rebuild_ipfs_placeholder_bcmr_partial_inputs(bcmr), _ => Vec::new(), }; let middle = Builder::new() .push_slice(&rev_ido) .push_slice(&rev_authguard) .push_slice(¶meters.authguardLockingBytecode) .push_slice(&offering_bcmr_partial_inputs) .push_slice(&oToken_bcmr_partial_inputs) .push_slice(&REBUILD_IPFS_PLACEHOLDER_BCMR_FOR_GENESIS_WITH_AUTHGUARD_CONTRACT) .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::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::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(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 { let mut r = vec![bytes.len() as u8]; r.extend(bytes); r } else if bytes.len() < 255 { let mut r = vec![0x4c_u8, bytes.len() as u8]; r.extend(bytes); r } else { 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> { let encoded = bigint_to_vm_number(v); if encoded.len() > length { Err(anyhow::anyhow!("The value does not fit in the target length")) } else { Ok(pad_minimally_encoded_vm_number(&encoded, length)) } } pub fn decode_padded_vm_number(bytes: &[u8]) -> BigInt { vm_number_to_bigint(bytes) } pub fn vm_number_to_bigint(bytes: &[u8]) -> BigInt { if bytes.is_empty() { return BigInt::zero(); } // Copy bytes so we can modify the sign bit if necessary let mut data = bytes.to_vec(); 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[last_idx] &= 0x7F; } // Convert little-endian bytes to BigInt let magnitude = BigInt::from_bytes_le(Sign::Plus, &data); if is_negative { -magnitude } else { magnitude } } pub fn bigint_to_vm_number(n: &BigInt) -> Vec { if n.is_zero() { return vec![]; } // Get the absolute magnitude in little-endian bytes let (sign, mut bytes) = n.to_bytes_le(); // Check if the high bit of the last byte is set // In Bitcoin VM numbers, the high bit of the last byte is the sign bit. // If it's already set by the magnitude, we must add an extra 0x00 or 0x80 byte. if let Some(&last) = bytes.last() { if (last & 0x80) != 0 { // High bit is set; push a new byte to carry the sign if sign == Sign::Minus { bytes.push(0x80); } else { bytes.push(0x00); } } else if sign == Sign::Minus { // High bit was NOT set; we can safely flip it on the current last byte if let Some(last_mut) = bytes.last_mut() { *last_mut |= 0x80; } } } bytes } pub fn pad_minimally_encoded_vm_number(bin: &[u8], length: usize) -> Vec { if bin.len() >= length { return bin.to_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, // just pad with zeros. padded.resize(length, 0x00); } } else { // Input was empty (effectively 0), pad with zeros padded.resize(length, 0x00); } padded } pub async fn prepare_tables(pool: &SqlitePool) { sqlx::query( "CREATE TABLE ido ( internal_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, preinit_txid BLOB NOT NULL UNIQUE, init_txid BLOB NULL, launch_txid BLOB NULL, offering_token_id BLOB NULL UNIQUE, offered_token_id BLOB NULL, status VARCHAR(20) NOT NULL, -- status: -- - PREINIT -- - ACTIVE -- - DISTRIBUTING -- - DISTRIBUTED parameters BLOB NOT NULL, state BLOB NOT NULL, txchain_entrypoint INTEGER NULL, txchain_head INTEGER NULL, is_valid INTEGER NOT NULL, is_token_created_at_preinit INTEGER NOT NULL )", ) .execute(pool) .await .expect("failed to create ido table"); sqlx::query( "CREATE TABLE ido_entry ( ido_id INTEGER NOT NULL, txid BLOB NOT NULL PRIMARY KEY, owner_nfthash BLOB NOT NULL, commitment BLOB, supply_amount INTEGER NOT NULL, demand_amount INTEGER NOT NULL, lockup_timeval INTEGER NOT NULL, discount INTEGER NOT NULL, distributed INTEGER NOT NULL )", ) .execute(pool) .await .expect("failed to create ido_entry table"); sqlx::query("CREATE INDEX idx_ido_entry_ido_id ON ido_entry(ido_id)") .execute(pool) .await .expect("failed to create index ido_entry(ido_id)"); sqlx::query( "CREATE TABLE ido_txchain ( id INTEGER NOT NULL PRIMARY KEY, prev_id INTEGER NULL, ido_id INTEGER NOT NULL REFERENCES ido(internal_id), txid BLOB NOT NULL UNIQUE, tx BLOB NOT NULL )", ) .execute(pool) .await .expect("failed to create ido_txchain table"); sqlx::query("CREATE INDEX idx_ido_txchain_ido_id ON ido_txchain(ido_id)") .execute(pool) .await .expect("failed to create index ido_txchain(ido_id)"); sqlx::query( "CREATE TABLE ido_txchain_tracker_map ( txid BLOB NOT NULL PRIMARY KEY, next_output_index INTEGER NULL, height INTEGER NOT NULL, txchain_id INTEGER NOT NULL )", ) .execute(pool) .await .expect("failed to create ido_txchain_tracker_map table"); } /*NOTUSED pub struct IdoEntryDBRecord { ido_id: i64, txid: Vec, owner_nfthash: Vec, commitment: Vec, supply_amount: u64, demand_amount: u64, lockup_timeval: u64, discount: u64, distributed: bool, } */ #[derive(Debug, Clone, serde::Serialize)] pub struct IdoDBRecord { internal_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, is_valid: bool, is_token_created_at_preinit: bool, } impl IdoDBRecord { fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result { Ok(Self { internal_id: row.get(0), preinit_txid: row.get(1), init_txid: row.get(2), launch_txid: row.get(3), offering_token_id: row.get(4), 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), is_token_created_at_preinit: row.get(12), }) } } #[derive(Debug, Clone, serde::Serialize)] pub struct IdoTxChainDBRecord { id: i64, prev_id: Option, ido_id: i64, txid: Vec, tx: Vec, } impl IdoTxChainDBRecord { fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result { Ok(Self { id: row.get(0), prev_id: row.get(1), ido_id: row.get(2), txid: row.get(3), tx: row.get(4), }) } } async fn get_ido_txchain_list( pool: &SqlitePool, internal_id: i64, ) -> Result> { sqlx::query( "SELECT id, prev_id, ido_id, txid, tx FROM ido_txchain WHERE ido_id = ?" ) .bind(internal_id) .fetch_all(pool) .await? .iter() .map(|a| IdoTxChainDBRecord::from_row(a)) .collect() } async fn txchain_lookup_next( pool: &SqlitePool, txid: &Txid, index: u32, ) -> Result> { 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_txchain t1 ON t0.txchain_id = t1.id WHERE t0.txid = ? AND t0.next_output_index = ?", ) .bind(txid.to_blob()) .bind(index) .fetch_optional(pool) .await?; if let Some(row) = row { Ok(Some(IdoTxChainDBRecord::from_row(&row)?)) } else { Ok(None) } } 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, internal_id: i64, ) -> Result> { let row = sqlx::query( "SELECT internal_id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, status, parameters, state, txchain_entrypoint, txchain_head, is_valid, is_token_created_at_preinit FROM ido WHERE internal_id = ?", ) .bind(internal_id) .fetch_optional(pool) .await?; if let Some(row) = row { Ok(Some(IdoDBRecord::from_row(&row)?)) } else { Ok(None) } } const TRACKER_MAX_BLOCK_DEPTH: i32 = 1000; async fn txchain_trim_tracker( pool: &SqlitePool, current_block_height: i64, ) -> Result<()> { 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(()) } const IDO_PREINIT_ANNOUNCEMENT_SIGNATURE: &[u8; 15] = &[ 0x6a, 0x4c, 0xbb, // OP_RETURN OP_PUSHDATA1 (187) 0x43, 0x61, 0x75, 0x6c, 0x64, 0x72, 0x6f, 0x6e, 0x49, 0x64, 0x6f, 0x30, // CauldronIdo0 ]; fn is_preinit_broadcast( tx: &Transaction, ) -> bool { 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 } } fn deserialize_ipfs_bcmr_with_placeholder( bcmr_data: &[u8], ) -> Result> { if bcmr_data.len() == 0 || (bcmr_data[0] == 0 && bcmr_data.len() == 1) { return Ok(None); } if bcmr_data.len() == 0 || !(bcmr_data[0] > 0 && bcmr_data[0] < 75) || bcmr_data[0].to_usize().unwrap() >= bcmr_data.len() { return Err(anyhow::anyhow!("bcmr needs metadata!")); } let metadata_size = bcmr_data[0].to_usize().unwrap(); let metadata_script = Script::from(bcmr_data[1..(metadata_size+1)].to_vec()); let metadata_inst_list: Vec<_> = metadata_script.instructions().collect(); if metadata_inst_list.len() != 2 { return Err(anyhow::anyhow!("bcmr metadata should have two push opcodes!")); } let registryReplaceCalls = match &metadata_inst_list[0] { Ok(Instruction::PushBytes(data)) => data.to_vec(), _ => { return Err(anyhow::anyhow!("oToken bcmr metadata opcode#0 is not a push opcode")); } }; let bcmrUrisReplaceCalls = match &metadata_inst_list[1] { Ok(Instruction::PushBytes(data)) => data.to_vec(), _ => { return Err(anyhow::anyhow!("oToken bcmr metadata opcode#0 is not a push opcode")); } }; let bcmr_opreturn: Vec<_> = bcmr_data[(metadata_size+1)..bcmr_data.len()].to_vec(); if bcmr_opreturn.len() < BCMR_SIGNATURE.len() + 32 || &bcmr_opreturn[0..BCMR_SIGNATURE.len()] != BCMR_SIGNATURE { return Err(anyhow::anyhow!("invalid bcmr opreturn")); } Ok(Some(IpfsBcmrWithPlaceholder { metadata: IpfsBcmrWithPlaceholderMetadata { registryReplaceCalls, bcmrUrisReplaceCalls, }, contentHash: bcmr_opreturn[BCMR_SIGNATURE.len()..BCMR_SIGNATURE.len()+32].to_vec(), urisBytecode: bcmr_opreturn[BCMR_SIGNATURE.len()+32..bcmr_opreturn.len()].to_vec(), })) } fn serialize_ipfs_bcmr_with_placeholder( input: &Option, ) -> Vec { match input { Some(value) => { let metadata_bytecode = Builder::new() .push_slice(&value.metadata.registryReplaceCalls) .push_slice(&value.metadata.bcmrUrisReplaceCalls) .into_script() .to_bytes(); let mut bcmr_bytecode = Vec::new(); bcmr_bytecode.extend_from_slice(&BCMR_SIGNATURE); bcmr_bytecode.extend_from_slice(&value.contentHash); bcmr_bytecode.extend_from_slice(&value.urisBytecode); let metadata_push = Builder::new() .push_slice(&metadata_bytecode) .into_script() .to_bytes(); let mut bytes = Vec::new(); bytes.extend_from_slice(&metadata_push); bytes.extend_from_slice(&bcmr_bytecode); bytes }, None => Vec::new(), } } fn deserialize_ipfs_bcmr_with_placeholder_from_p2s_storage(script: &Script) -> Result> { deserialize_ipfs_bcmr_with_placeholder( &extract_data_from_storage_script_with_data_and_size(script)? ) } struct IdoContext { preinit_txid: Vec, init_txid: Option>, launch_txid: Option>, status: String, parameters: IdoParameters, state: IdoState, is_valid_ido: bool, is_token_created_at_preinit: bool, offered_token_id: Option>, offering_token_id: Option>, } struct IdoPreinitParseParamsResult { preinit_parameters: IdoPreInitParameters, offered_token_is_in_supply: bool, is_valid_ido: bool, } fn parse_ido_preinit_tx_params(tx: &Transaction, delphi_token_id: &[u8], platform_fee_nfth: &[u8], errors: &mut Vec, invalid_ido_reasons: &mut Vec) -> Option { // preinit announcement let annOut = tx.output.get(0); // preinit params if annOut.is_some() { let instructions: Vec<_> = annOut.unwrap().script_pubkey.instructions().collect(); if instructions.len() != 3 { errors.push(anyhow::anyhow!("should have 3 opcodes")); return None; } // authguard locking bytecode let authguardLockingBytecode; match &instructions[2] { Ok(Instruction::PushBytes(data)) => { authguardLockingBytecode = data.to_vec(); } _ => { errors.push(anyhow::anyhow!("invalid announcement (3)")); return None; } } // params main match &instructions[1] { Ok(Instruction::PushBytes(data)) => { if data.len() < 187 { errors.push(anyhow::anyhow!("Incorrect announcement.mainData size")); return None; } let mut is_valid_ido = true; // preinit_parameters.offeredTokenTotalSupply < BigInt::from(0) let offered_token_is_in_supply = vm_number_to_bigint(&data[170..178]) < BigInt::from(0); let offeringBcmrStorageOut = tx.output.get(8); if offeringBcmrStorageOut.is_none() { errors.push(anyhow::anyhow!("offering bcmr storage not defined!")); return None; } let offeringBcmrResult = deserialize_ipfs_bcmr_with_placeholder_from_p2s_storage(&offeringBcmrStorageOut.unwrap().script_pubkey); if offeringBcmrResult.is_err() { errors.push(anyhow::anyhow!("offering bcmr deserialize failed, {}", offeringBcmrResult.err().unwrap())); return None; } let offeringBcmr = offeringBcmrResult.unwrap(); let mut oTokenBcmr: Option = None; if !offered_token_is_in_supply { let oTokenBcmrStorageOut = tx.output.get(9); if oTokenBcmrStorageOut.is_none() { errors.push(anyhow::anyhow!("oToken bcmr storage not defined!")); return None; } let oTokenBcmrResult = deserialize_ipfs_bcmr_with_placeholder_from_p2s_storage(&oTokenBcmrStorageOut.unwrap().script_pubkey); if oTokenBcmrResult.is_err() { errors.push(anyhow::anyhow!("oToken bcmr deserialize failed, {}", oTokenBcmrResult.err().unwrap())); return None; } else { oTokenBcmr = oTokenBcmrResult.unwrap(); } } let preinit_parameters = IdoPreInitParameters { preInitBcmr: IdoPreInitBcmrParameters { offering: offeringBcmr, oToken: oTokenBcmr, }, authguardLockingBytecode: authguardLockingBytecode, offering: IdoParametersOffering { platformFeeNFTH: data[12..44].to_vec(), platformFeeNumerator: vm_number_to_bigint(&data[44..48]), delphiCategory: data[48..80].to_vec(), launchConditions: IdoParametersOfferingLaunchConditions { expiresAt: vm_number_to_bigint(&data[80..86]), deployThreshold: vm_number_to_bigint(&data[86..94]), immediateDeployThreshold: vm_number_to_bigint(&data[94..102]), }, offer: IdoParametersOfferingOffer { maxDiscountRateNumerator: vm_number_to_bigint(&data[102..106]), discountAnnualRateNumerator: vm_number_to_bigint(&data[106..110]), priceNumerator: vm_number_to_bigint(&data[110..126]), minOffer: vm_number_to_bigint(&data[126..130]), xTokenCategory: if vm_number_to_bigint(&data[130..162]) == BigInt::from(0) { None } else { Some(data[130..162].to_vec()) }, }, executionFee: vm_number_to_bigint(&data[162..166]), }, permanentLiquidityShareNumerator: vm_number_to_bigint(&data[166..170]), offeredTokenTotalSupply: vm_number_to_bigint(&data[170..178]), offeredTokenAmount: vm_number_to_bigint(&data[178..186]), authguardNftOutputIndex: vm_number_to_bigint(&data[186..187]), }; // enforce limitation (xToken == NATIVE_BCH) if preinit_parameters.offering.offer.xTokenCategory.is_some() { errors.push(anyhow::anyhow!("non-null xTokenCategory is not supported")); return None; } if !offered_token_is_in_supply { let permanentLiquidityOTokenReserve: BigInt = &preinit_parameters.offeredTokenAmount * &preinit_parameters.permanentLiquidityShareNumerator / &PERMANENT_LIQUIDITY_SHARE_DENOMINATOR.clone(); if preinit_parameters.offeredTokenTotalSupply <= &preinit_parameters.offeredTokenAmount + &permanentLiquidityOTokenReserve { invalid_ido_reasons.push(anyhow::anyhow!("ido parse, not a valid ido, preinit_parameters.offeredTokenTotalSupply <= preinit_parameters.offeredTokenAmount + permanentLiquidityOTokenReserve")); is_valid_ido = false; } } if preinit_parameters.permanentLiquidityShareNumerator < *MIN_PLP_SHARE { invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.permanentLiquidityShareNumerator < *MIN_PLP_SHARE")); is_valid_ido = false; } if preinit_parameters.permanentLiquidityShareNumerator > *MAX_PLP_SHARE { invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.permanentLiquidityShareNumerator > *MAX_PLP_SHARE")); is_valid_ido = false; } if preinit_parameters.offering.offer.discountAnnualRateNumerator < *MIN_PLP_ANNUAL_DISCOUNT { invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.offer.discountAnnualRateNumerator > *MAX_PLP_ANNUAL_DISCOUNT")); is_valid_ido = false; } if preinit_parameters.offering.offer.discountAnnualRateNumerator > *MAX_PLP_ANNUAL_DISCOUNT { invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.offer.discountAnnualRateNumerator > *MAX_PLP_ANNUAL_DISCOUNT")); is_valid_ido = false; } if preinit_parameters.offering.offer.maxDiscountRateNumerator < BigInt::from(0) { invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.offer.maxDiscountRateNumerator < BigInt::from(0)")); is_valid_ido = false; } if &preinit_parameters.offering.offer.maxDiscountRateNumerator + &MIN_PLP_AFTER_DISCOUNT.clone() > preinit_parameters.permanentLiquidityShareNumerator { invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.offer.maxDiscountRateNumerator + MIN_PLP_AFTER_DISCOUNT > preinit_parameters.permanentLiquidityShareNumerator")); is_valid_ido = false; } if preinit_parameters.offeredTokenAmount <= BigInt::from(0) { invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offeredTokenAmount <= BigInt::from(0)")); is_valid_ido = false; } if preinit_parameters.offering.delphiCategory != delphi_token_id { invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.delphiCategory != DELPHI_TOKEN_ID")); is_valid_ido = false; } if &preinit_parameters.offering.platformFeeNFTH != platform_fee_nfth { invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.platformFeeNFTH != platform_fee_nfth")); is_valid_ido = false; } if preinit_parameters.offering.platformFeeNumerator < *PLATFORM_FEE { invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.platformFeeNumerator < *PLATFORM_FEE")); is_valid_ido = false; } if preinit_parameters.offering.executionFee < *ENTRY_EXECUTION_FEE { invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.executionFee < *ENTRY_EXECUTION_FEE")); is_valid_ido = false; } return Some(IdoPreinitParseParamsResult { preinit_parameters, offered_token_is_in_supply, is_valid_ido, }); } _ => { errors.push(anyhow::anyhow!("invalid announcement (2)")); return None; } } } else { errors.push(anyhow::anyhow!("announcement output is missing")); return None; } } struct IdoPreinitParseResult { parameters: Option, state: Option, is_valid_ido: bool, offered_token_is_in_supply: bool, offered_token_id: Option>, } fn parse_ido_preinit_tx(network: Option, tx: &Transaction, errors: &mut Vec, invalid_ido_reasons: &mut Vec) -> IdoPreinitParseResult { 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 delphi_token_id = match network { Some(Network::Chipnet) => CHIPNET_DELPHI_TOKEN_ID.clone(), Some(Network::Bitcoin) => MAINNET_DELPHI_TOKEN_ID.clone(), _ => MAINNET_DELPHI_TOKEN_ID.clone(), }; let offered_token_is_in_supply: bool; let mut is_valid_ido: bool; let nullable_preinit_parameters: Option; // preinit announcement match parse_ido_preinit_tx_params(tx, &delphi_token_id, &platform_fee_nfth, errors, invalid_ido_reasons) { Some(output) => { offered_token_is_in_supply = output.offered_token_is_in_supply; is_valid_ido = output.is_valid_ido; nullable_preinit_parameters = Some(output.preinit_parameters); } None => { offered_token_is_in_supply = false; is_valid_ido = false; nullable_preinit_parameters = None; } } let mut authguardNftOut = None; // find authguardCategory if let Some(params) = nullable_preinit_parameters { if params.authguardNftOutputIndex >= BigInt::from(0) { match params.authguardNftOutputIndex.to_usize() { Some(index) => { authguardNftOut = tx.output.get(index); if authguardNftOut.is_none() { errors.push(anyhow::anyhow!("authguardNftOutputIndex out of range")); } }, _ => errors.push(anyhow::anyhow!("authguardNftOutputIndex out of range")), } } let 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) }, }; let preInitOut = tx.output.get(1); if preInitOut.is_none() { errors.push(anyhow::anyhow!("preinit output is not defined!")); } else { let preInit_lock_parameters = IdoPreInitLockParameters { preInitBcmr: params.preInitBcmr.clone(), authguardLockingBytecode: params.authguardLockingBytecode.clone(), permanentLiquidityShareNumerator: params.permanentLiquidityShareNumerator.clone(), offeredTokenAmount: params.offeredTokenAmount.clone(), offeredTokenTotalSupply: params.offeredTokenTotalSupply.clone(), }; let bytecode = build_preinit_bytecode(&preInit_lock_parameters, &state); if build_p2sh32_script(&bytecode) != preInitOut.unwrap().script_pubkey { errors.push(anyhow::anyhow!("preInit output bytecode does not match, expected bytecode: {}", hex::encode(bytecode))); } } let offeringBytecodeStorageOut = tx.output.get(2); if offeringBytecodeStorageOut.is_none() { errors.push(anyhow::anyhow!("offering bytecode storage not defined!")); } else { let bytecode = build_storage_script_with_data_and_size( 1, &build_offering_bytecode( ¶ms.offering.offer.maxDiscountRateNumerator, ¶ms.offering.offer.discountAnnualRateNumerator, ¶ms.offering.offer.priceNumerator, ¶ms.offering.offer.minOffer, params.offering.offer.xTokenCategory.as_deref().unwrap_or(&[]), ) ).to_bytes(); if build_p2sh32_script(&bytecode) != offeringBytecodeStorageOut.unwrap().script_pubkey { errors.push(anyhow::anyhow!("offering bytecode storage does not match, expected bytecode: {}", hex::encode(bytecode))); } } let launcherBytecodeStorageOut = tx.output.get(3); if launcherBytecodeStorageOut.is_none() { errors.push(anyhow::anyhow!("launcher bytecode storage not defined!")); } else { let bytecode = build_storage_script_with_data_and_size( 1, &build_launcher_bytecode( &vec![0u8; 32], // collectorNFTH ¶ms.offering.platformFeeNFTH, ¶ms.offering.platformFeeNumerator, ¶ms.offering.delphiCategory, ¶ms.offering.launchConditions.expiresAt, ¶ms.offering.launchConditions.deployThreshold, ¶ms.offering.launchConditions.immediateDeployThreshold, ) ).to_bytes(); if build_p2sh32_script(&bytecode) != launcherBytecodeStorageOut.unwrap().script_pubkey { errors.push(anyhow::anyhow!("launcher bytecode storage does not match, expected bytecode: {}", hex::encode(bytecode))); } } let distDeployBytecodeStorageOut = tx.output.get(4); if distDeployBytecodeStorageOut.is_none() { errors.push(anyhow::anyhow!("dist deploy bytecode storage not defined!")); } else { let bytecode = build_storage_script_with_data_and_size( 1, &DISTRIBUTOR_DEPLOY_CONTRACT, ).to_bytes(); if build_p2sh32_script(&bytecode) != distDeployBytecodeStorageOut.unwrap().script_pubkey { errors.push(anyhow::anyhow!("dist deploy bytecode storage does not match, expected bytecode: {}", hex::encode(bytecode))); } } let distRefundBytecodeStorageOut = tx.output.get(5); if distRefundBytecodeStorageOut.is_none() { errors.push(anyhow::anyhow!("dist refund bytecode storage not defined!")); } else { let bytecode = build_storage_script_with_data_and_size( 1, &DISTRIBUTOR_REFUND_CONTRACT, ).to_bytes(); if build_p2sh32_script(&bytecode) != distRefundBytecodeStorageOut.unwrap().script_pubkey { errors.push(anyhow::anyhow!("dist refund bytecode storage does not match, expected bytecode: {}", hex::encode(bytecode))); } } let offeringInitiatorBytecodeStorageOut = tx.output.get(6); if offeringInitiatorBytecodeStorageOut.is_none() { errors.push(anyhow::anyhow!("offering initiator bytecode storage not defined!")); } else { let bytecode = build_storage_script_with_data_and_size( 1, &build_partial_offering_initiator_bytecode( &BigInt::from(7i64), // extensionOutpointIndex &BigInt::from(6i64), // tokenStorageOutpointIndex &BigInt::from(5i64), // offeringBcmrStorageOutpointIndex &BigInt::from(4i64), // distRefundOutpointIndex &BigInt::from(3i64), // distDeployOutpointIndex &BigInt::from(2i64), // launcherBytecodeOutpointIndex &BigInt::from(1i64), // offeringBytecodeOutpointIndex ¶ms.offering.executionFee, ), ).to_bytes(); if build_p2sh32_script(&bytecode) != offeringInitiatorBytecodeStorageOut.unwrap().script_pubkey { errors.push(anyhow::anyhow!("offering initiator bytecode storage does not match, expected bytecode: {}", hex::encode(bytecode))); } } let idoInitiatorBytecodeStorageOut = tx.output.get(7); if idoInitiatorBytecodeStorageOut.is_none() { errors.push(anyhow::anyhow!("ido initiator bytecode storage not defined!")); } else { let bytecode = build_storage_script_with_data_and_size( 1, &build_partial_ido_initiator_bytecode( &build_partial_postlaunch_bytecode( ¶ms.permanentLiquidityShareNumerator, ¶ms.offering.offer.priceNumerator, ), &BigInt::from(8i64), // permanentPoolOTokenReserveOutpointIndex &BigInt::from(0i64), // offeringInitiatorOutpointIndex ), ).to_bytes(); if build_p2sh32_script(&bytecode) != idoInitiatorBytecodeStorageOut.unwrap().script_pubkey { errors.push(anyhow::anyhow!("ido initiator bytecode storage does not match, expected bytecode: {}", hex::encode(bytecode))); } } let offeringBcmrStorageOut = tx.output.get(8); if offeringBcmrStorageOut.is_none() { errors.push(anyhow::anyhow!("offered token bcmr storage not found!")); } else { let bytecode = build_storage_script_with_data_and_size( 1u32, &serialize_ipfs_bcmr_with_placeholder(¶ms.preInitBcmr.offering), ); if bytecode != offeringBcmrStorageOut.unwrap().script_pubkey { errors.push(anyhow::anyhow!("offering bcmr storage locking bytecode does not match, expected bytecode: {}", hex::encode(bytecode))); } } let mut offered_token_id: Option> = None; if offered_token_is_in_supply { // verify offered token storage let offeredTokenStorageOut = tx.output.get(9); if offeredTokenStorageOut.is_none() { errors.push(anyhow::anyhow!("offered token supply not found!")); } else { let permanentLiquidityOTokenReserve: BigInt = ¶ms.offeredTokenAmount * ¶ms.permanentLiquidityShareNumerator / &PERMANENT_LIQUIDITY_SHARE_DENOMINATOR.clone(); let requiredTokens = ¶ms.offeredTokenAmount + permanentLiquidityOTokenReserve; if offeredTokenStorageOut.unwrap().token.is_none() { errors.push(anyhow::anyhow!("offered token storage does not contain a token!")); } else { let bytecode = build_storage_script(1u32).to_bytes(); if build_p2sh32_script(&bytecode) != offeredTokenStorageOut.unwrap().script_pubkey { errors.push(anyhow::anyhow!("offered token locking bytecode does not match, expected bytecode: {}", hex::encode(bytecode))); } let token_amount = BigInt::from(offeredTokenStorageOut.unwrap().token.as_ref().unwrap().amount); if token_amount < requiredTokens { errors.push(anyhow::anyhow!("invalid offered token amount!")); } offered_token_id = Some(offeredTokenStorageOut.unwrap().token.as_ref().unwrap().id.to_blob()); } } } else { let oTokenBcmrStorageOut = tx.output.get(9); if oTokenBcmrStorageOut.is_none() { errors.push(anyhow::anyhow!("offered token bcmr storage not found!")); } else { let bytecode = build_storage_script_with_data_and_size( 1u32, &serialize_ipfs_bcmr_with_placeholder(¶ms.preInitBcmr.oToken), ); if bytecode != oTokenBcmrStorageOut.unwrap().script_pubkey { errors.push(anyhow::anyhow!("offered token bcmr storage locking bytecode does not match, expected bytecode: {}", hex::encode(bytecode))); } } } // collect preinit fee let mut preinit_paid_fee: u64 = 0; for output in &tx.output { if build_p2sh32_script( &build_p2nfth_script(&platform_fee_nfth).to_bytes() ) == output.script_pubkey { preinit_paid_fee += output.value; } } if BigInt::from(preinit_paid_fee) < *IDO_CREATE_EXECUTION_FEE { is_valid_ido = false; invalid_ido_reasons.push(anyhow::anyhow!("ido parse, not enough preinit fee paid!")); } IdoPreinitParseResult { parameters: Some(params), state: Some(state), is_valid_ido, offered_token_is_in_supply, offered_token_id, } } else { IdoPreinitParseResult { parameters: None, state: None, is_valid_ido, offered_token_is_in_supply: false, offered_token_id: None, } } } fn create_ido_context_from_preinit(network: Option, tx: &Transaction, errors: &mut Vec, invalid_ido_reasons: &mut Vec) -> Result { let result = parse_ido_preinit_tx(network, tx, errors, invalid_ido_reasons); if errors.len() > 0 { Err(anyhow::anyhow!("Failed to parse the preinit tx!")) } else { let params = result.parameters.expect("parameters should be defined!"); let state = result.state.expect("state should be defined!"); Ok(IdoContext { preinit_txid: tx.txid().to_blob(), init_txid: None, launch_txid: None, status: "PREINIT".to_string(), parameters: IdoParameters::PreInit(params), state: IdoState::PreInit(state), is_valid_ido: result.is_valid_ido, offered_token_id: result.offered_token_id, offering_token_id: None, is_token_created_at_preinit: !result.offered_token_is_in_supply, }) } } async fn on_create_ido( network: Option, pool: &SqlitePool, tx: &Transaction, block_height: i64, ) -> Result<()> { let mut errors: Vec = Vec::new(); let mut invalid_ido_reasons: Vec = Vec::new(); let result = create_ido_context_from_preinit(network, tx, &mut errors, &mut invalid_ido_reasons); if errors.len() > 0 { info!("Parse ido preinit failed, txid: {}\nError(s):", hex::encode(tx.txid().to_blob())); for error in errors { info!(" - {}", error); } } if invalid_ido_reasons.len() > 0 { info!("Invalid ido found, preinit_txid: {}\nReason(s):", hex::encode(tx.txid().to_blob())); for reason in invalid_ido_reasons { info!(" - {}", reason); } } match result { Ok(context) => { // create the ido let mut dbtx = pool.begin().await?; let result = sqlx::query( "INSERT INTO ido (preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, status, parameters, state, is_valid, is_token_created_at_preinit, txchain_entrypoint, txchain_head) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(context.preinit_txid) .bind(context.init_txid) .bind(context.launch_txid) .bind(context.offering_token_id) .bind(context.offered_token_id) .bind(context.status) .bind(serde_json::to_vec(&context.parameters).unwrap()) .bind(serde_json::to_vec(&context.state).unwrap()) .bind(context.is_valid_ido) .bind(context.is_token_created_at_preinit) .bind(None::) .bind(None::) .execute(&mut *dbtx) .await; match result { Ok(r) => { // insert txchain entrypoint let internal_id = r.last_insert_rowid(); let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, internal_id, None::, block_height, 1) .await?; sqlx::query( "UPDATE ido SET txchain_entrypoint = ?, txchain_head = ? WHERE internal_id = ?", ) .bind(txchain_item_id) .bind(txchain_item_id) .bind(internal_id) .execute(&mut *dbtx) .await?; dbtx.commit().await?; Ok(()) }, Err(e) => Err(e.into()), } }, Err(e) => Err(e), } } fn is_preinit_state_ready_to_init(state: &IdoPreInitState) -> bool { state.oTokenGenerated != BigInt::from(0) && state.nextTxSetterFlag == BigInt::from(0) && state.authguardCategory.iter().any(|&b| b != 0) && state.idoCategory.iter().any(|&b| b != 0) } fn parse_preinit_state_from_tx( tx: &Transaction, ) -> Result { // preinit + offering iniitation let preInitIn = tx.input.get(1); if preInitIn.is_none() { // panic return Err(anyhow::anyhow!("Not a valid preinit tx!")); } 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), OfferedTokenId(Vec), OfferingTokenId(Vec), Entry(IdoUpdateEntry), EntryDistributed { txid: Vec, }, } struct IdoAddResult { updates: Vec, next_output_index: i32, } fn ido_add_tx( context: &IdoContext, tx: &Transaction, ) -> Result { // inputs to check #0, #1, #3 let mut updates: Vec = Vec::new(); match context.status.as_str() { "PREINIT" => { 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) .ok_or_else(|| anyhow::anyhow!("launcher bytecode storage does not exist!"))?; let launcher_data = extract_data_from_unlocking_bytecode_of_storage_with_data_and_size(&launcherBytecodeIn.script_sig) .map_err(|e| anyhow::anyhow!("failed to extract launcher bytecode: {e}"))?; let launcher_script = Script::from(launcher_data); let launcher_inst_list: Vec<_> = launcher_script.instructions().collect(); let collectorNFTH: Vec; if launcher_inst_list.len() == 0 { return Err(anyhow::anyhow!("empty launcher bytecode!")); } match &launcher_inst_list[0] { Ok(Instruction::PushBytes(data)) => { collectorNFTH = data.to_vec(); } _ => return Err(anyhow::anyhow!("expecting push opcode for collectorNFTH")), } let preinit_params: &IdoPreInitParameters; match &context.parameters { IdoParameters::PreInit(value) => { preinit_params = value; } _ => return Err(anyhow::anyhow!("expecting PreInit parameters")), } updates.push(IdoUpdate::Parameters(IdoParameters::Active(IdoActiveParameters { preInitBcmr: preinit_params.preInitBcmr.clone(), 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).ok_or_else(|| anyhow::anyhow!("token storage does not exist!"))?; let offeringOut = tx.output.get(1).ok_or_else(|| anyhow::anyhow!("offering does not exist!"))?; if tokenStorageOut.token.is_none() || offeringOut.token.is_none() { return Err(anyhow::anyhow!("output#1 & output#2 should have token!")); } updates.push(IdoUpdate::OfferedTokenId(tokenStorageOut.token.as_ref().unwrap().id.to_blob())); updates.push(IdoUpdate::OfferingTokenId(offeringOut.token.as_ref().unwrap().id.to_blob())); return Ok(IdoAddResult { updates: updates, next_output_index: 0, // offering utxo }); } } else { 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).ok_or_else(|| anyhow::anyhow!("Should have first output!"))?; 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.as_ref().unwrap().commitment.len() == 0 { return Err(anyhow::anyhow!("Incorrect commitment size at output#0, should be greater than zero")); } if first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS == ITEM_TYPE_OFFERING { // enforce limitation (xToken == NATIVE_BCH) 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).ok_or_else(|| anyhow::anyhow!("Should have the second output!"))?; 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 first_input = tx.input.get(0).ok_or_else(|| anyhow::anyhow!("Should have first input!"))?; let instructions: Vec<_> = first_input.script_sig.instructions().collect(); if instructions.len() == 0 { return Err(anyhow::anyhow!("empty unlocking bytecode!")); } match &instructions[0] { Ok(Instruction::PushBytes(data)) => { owner_nfthash = data.to_vec(); }, _ => return Err(anyhow::anyhow!("OP_PUSH expected in the unlocking bytecode of the offering utxo!")), } if first_output.token.as_ref().unwrap().commitment.len() < 9 { return Err(anyhow::anyhow!("Incorrect commitment size at output#0")); } if second_output.token.as_ref().unwrap().commitment.len() < 15 { return Err(anyhow::anyhow!("Incorrect commitment size at output#1")); } updates.push(IdoUpdate::Entry(IdoUpdateEntry { txid: tx.txid().to_blob(), owner_nfthash: owner_nfthash, supply_amount: second_output.value, 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(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.as_ref().unwrap().commitment.clone(); if dist_commitment.len() < 35 { return Err(anyhow::anyhow!("Incorrect commitment size at output#0")); } 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_commitment[27..35]), platformEarnedAmount: BigInt::from(0), }) )); } 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.as_ref().unwrap().commitment.clone(); if conf_commitment.len() < 31 { return Err(anyhow::anyhow!("Incorrect commitment size at output#0")); } 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::from(0), }) )); } else { return Err(anyhow::anyhow!("output#0 is not recognized!")); } return Ok(IdoAddResult { updates: updates, next_output_index: 0, }); }, "DISTRIBUTING" => { 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).ok_or_else(|| anyhow::anyhow!("Should have first output!"))?; 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 { IdoState::Distributing(value) => { prev_state = value; } _ => return Err(anyhow::anyhow!("expecting distributing state")), } let platform_fee_output = tx.output.get(4).ok_or_else(|| anyhow::anyhow!("platform fee output does not exist!"))?; if first_output.token.as_ref().unwrap().commitment.len() == 0 { return Err(anyhow::anyhow!("Incorrect commitment size at output#0, should be greater than zero")); } if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_DISTRIBUTOR { // output#0 distributor let dist_commitment = first_output.token.as_ref().unwrap().commitment.clone(); if dist_commitment.len() < 35 { return Err(anyhow::anyhow!("Incorrect dist_commitment size at output#0")); } 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_commitment[27..35]), platformEarnedAmount: &prev_state.platformEarnedAmount + BigInt::from(platform_fee_output.value), }) )); } 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.as_ref().unwrap().commitment.clone(); if conf_commitment.len() < 31 { return Err(anyhow::anyhow!("Incorrect conf_commitment size at output#0")); } 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), }) )); } else { return Err(anyhow::anyhow!("output#0 is not recognized!")); } return Ok(IdoAddResult { updates: updates, next_output_index: 0, }); }, "POSTLAUNCH" => { let pp_output = tx.output.get(0).ok_or_else(|| anyhow::anyhow!("Should have first output!"))?; let collector_p2nfth = tx.output.get(2).ok_or_else(|| anyhow::anyhow!("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(anyhow::anyhow!("expecting postlaunch state")), } updates.push(IdoUpdate::State( IdoState::Distributed(IdoDistributedState { permanentPool: if pp_output.token.is_none() { None } else { Some(IdoPermanentPoolV0 { tokenAmount: BigInt::from(pp_output.token.as_ref() .ok_or_else(|| anyhow::anyhow!("pp should have tokens"))? .amount), satoshiAmount: BigInt::from(pp_output.value), }) }, refundAmount: BigInt::from(collector_p2nfth.token.as_ref() .ok_or_else(|| anyhow::anyhow!("collector_p2nfth should have tokens"))? .amount), 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(anyhow::anyhow!( "An ido with an unknown status: {}", context.status )), } } async fn update_ido( dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, internal_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 internal_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(internal_id) .execute(&mut **dbtx) .await?; Ok(()) } fn apply_updates_to_context( context: &mut IdoContext, updates: &[IdoUpdate], ) -> () { for update in updates { match update { IdoUpdate::InitTxId(txid) => { context.init_txid = Some(txid.clone()); }, IdoUpdate::LaunchTxId(txid) => { context.launch_txid = Some(txid.clone()); }, IdoUpdate::Status(status) => { context.status = status.clone(); }, IdoUpdate::State(state) => { context.state = state.clone(); }, IdoUpdate::Parameters(parameters) => { context.parameters = parameters.clone(); }, IdoUpdate::OfferedTokenId(token_id) => { context.offered_token_id = Some(token_id.clone()); }, IdoUpdate::OfferingTokenId(token_id) => { context.offering_token_id = Some(token_id.clone()); }, _ => {} } } } async fn upsert_updates_to_ido_entries( dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, internal_id: i64, updates: &[IdoUpdate], ) -> Result<()> { 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.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(txid); } }, _ => {} } } for entry in insert_list { sqlx::query( "INSERT INTO ido_entry (ido_id, txid, owner_nfthash, commitment, supply_amount, demand_amount, lockup_timeval, discount, distributed) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(internal_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_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: &mut sqlx::Transaction<'_, sqlx::Sqlite>, tx: &Transaction, internal_id: i64, prev_txchain_id: Option, block_height: i64, next_output_index: i32, ) -> Result { let serialized_tx = bitcoincash::consensus::serialize(tx); let result = sqlx::query( "REPLACE INTO ido_txchain (prev_id, ido_id, txid, tx) VALUES (?, ?, ?, ?)" ) .bind(prev_txchain_id) .bind(internal_id) .bind(tx.txid().to_blob()) .bind(serialized_tx) .execute(&mut **dbtx) .await?; let txchain_item_id: i64 = result.last_insert_rowid(); if next_output_index >= 0 { 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?; } Ok(txchain_item_id) } async fn update_ido_txchain_tracker_block_height( dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, tx: &Transaction, block_height: i64, ) -> Result<()> { // only update block_height 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, tx: &Transaction, block_height: i64, ) -> Result<()> { let mut dbtx = pool.begin().await?; 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(&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 = get_ido_txchain_list(pool, ido.internal_id) .await?; let mut items_tx_map: HashMap, Transaction> = HashMap::new(); for item in &items { let tx_deser: Transaction = bitcoincash::consensus::deserialize(&item.tx).map_err(|e| anyhow::anyhow!("failed to deserialize tx: {e}"))?; items_tx_map.insert(item.txid.clone(), tx_deser); } 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).ok_or_else(|| anyhow::anyhow!("txchain prev item not found!"))?; current_item = items_copy.remove(found_index); new_chain.insert(0, current_item); } if new_chain.get(0).ok_or_else(|| anyhow::anyhow!("txchain is empty"))?.txid != ido.preinit_txid { return Err(anyhow::anyhow!("txchain entrypoint does not match preinit_txid")); } // start from PREINIT let preinit_tx = items_tx_map.get(&ido.preinit_txid).ok_or_else(|| anyhow::anyhow!("ido's entrypoint does not exist in the txchain"))?; let mut _errors: Vec = Vec::new(); let mut _invalid_ido_reasons: Vec = Vec::new(); let mut context = create_ido_context_from_preinit(network, preinit_tx, &mut _errors, &mut _invalid_ido_reasons)?; let mut updates: Vec = Vec::new(); for i in 1..new_chain.len() { let item = new_chain.get(i).ok_or_else(|| anyhow::anyhow!("txchain item not found at index"))?; let item_tx = items_tx_map.get(&item.txid).ok_or_else(|| anyhow::anyhow!("txchain tx not found in map"))?; // 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.internal_id, Some(prev_txchain_item.id), block_height, result.next_output_index) .await?; update_ido(&mut dbtx, ido.internal_id, &context, txchain_item_id) .await?; // delete all entries sqlx::query( "DELETE FROM ido_entry WHERE ido_id = ?", ) .bind(ido.internal_id) .execute(&mut *dbtx) .await?; // insert the entries of the new state upsert_updates_to_ido_entries(&mut dbtx, ido.internal_id, &updates) .await?; } else { // create context from ido let parameters: IdoParameters = serde_json::from_slice(&ido.parameters) .map_err(|e| anyhow::anyhow!("Failed to parse parameters: {e}"))?; let state: IdoState = serde_json::from_slice(&ido.state) .map_err(|e| anyhow::anyhow!("Failed to parse state: {e}"))?; let mut context: IdoContext = IdoContext { preinit_txid: ido.preinit_txid.clone(), init_txid: ido.init_txid.clone(), launch_txid: ido.launch_txid.clone(), status: ido.status.clone(), parameters, state, is_valid_ido: ido.is_valid, is_token_created_at_preinit: ido.is_token_created_at_preinit, offered_token_id: ido.offered_token_id.clone(), offering_token_id: ido.offering_token_id.clone(), }; // add to the chain 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.internal_id, Some(prev_txchain_item.id), block_height, result.next_output_index) .await?; update_ido(&mut dbtx, ido.internal_id, &context, txchain_item_id) .await?; upsert_updates_to_ido_entries(&mut dbtx, ido.internal_id, &result.updates) .await?; } dbtx.commit().await?; Ok(()) } 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() { if has_script_ido_signature(&input.unwrap().script_sig) { return true; } } } return false; } pub async fn index_block( network: Option, pool: &SqlitePool, sorted_txs: &[Transaction], _blockhash: &BlockHash, _mtp: i64, block_height: i64, ) -> Result<()> { for tx in sorted_txs { // detect a new ido if is_preinit_broadcast(tx) { match on_create_ido(network, pool, tx, block_height) .await { Err(err) => info!("failed to detect an ido, or an invalid ido detected, txid: {}, {}", hex::encode(tx.txid().to_blob()), err), _ => (), } } 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() { if let Some(txchain_item) = txchain_lookup_next( pool, &input.unwrap().previous_output.txid, input.unwrap().previous_output.vout ) .await? { match ido_lookup(pool, txchain_item.ido_id) .await { Ok(Some(ido)) => { match on_add_ido_tx(network, pool, &ido, &txchain_item, tx, block_height) .await { Err(err) => info!("add tx to an ido failed, txid: {}, {}", hex::encode(tx.txid().to_blob()), err), _ => (), } }, Err(err) => warn!("ido record not found!!, {}", err), _ => warn!("ido record not found!!"), } break; } } } } } txchain_trim_tracker(pool, block_height) .await?; Ok(()) } // ─── Public RPC types ──────────────────────────────────────────────────────── #[derive(Serialize, Clone)] pub struct IdoRpcRecord { pub id: String, 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: String, 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: String, pub ido_internal_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 { let preinit_txid_hex = blob_to_display_hex::(&self.preinit_txid)?; Ok(IdoRpcRecord { id: preinit_txid_hex.clone(), preinit_txid: preinit_txid_hex, 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, }) } } #[cfg(test)] mod tests { use super::*; use bitcoincash::blockdata::transaction::Transaction; use std::sync::LazyLock; static PREINIT_TEST_TX01: LazyLock> = LazyLock::new(|| hex::decode("0200000001e6b6c7e14eb71e53285f252a3ec38a4822befbe5ed15c18303c53c593af14b5900000000644167d23b1ba69109557b9d2efd7c20bc96e794fba690e33cee5b89e3db536045bd0fa04ff78b7e9b1c48b308b88355e2ffc1d92207c5da4c0d72a48765c4bdf88c612103e88d9ab230288089c7363b77431504661a943266c08235cb1ac3998349b75e67000000000c0000000000000000d86a4cbb4361756c64726f6e49646f30072d5fbecfa1bc37a13f58b188f50a5bc1dfb9ab8ae0bf5d3d9a63f6f81efc0b809698001b948240c9cfaa8224383db16e4735efa09c9426d1713d265e849a8e91f2669afefc0a6a000070170000000000007017000000000000002d310180969800041a130d8401000000000000000000007017000000000000000000000000000000000000000000000000000000000000000000001027000080c3c90100e40b540200000000ca9a3b00000000811976a91430da60e54a4d87ecdcb6adf91a98736668b55e9e88ace80300000000000023aa20d82b9eb9f86f02acd8787d6959d24e2a81e8e8ffcf9fae67d94e3b13ab74f6fe87e80300000000000023aa20b0be53828f2700b0fd254708e0b54b100163be607c1c82138ba793a2d667e55f87e80300000000000023aa20542b74123c493fc2f1f74d99586f87355b07c5a450b64111085759cf9cc96d8287e80300000000000023aa20f69ff67df71b9ecd4b6b39af393551468bb2a46fb261e2e5a61cfb27dbcbb25087e80300000000000023aa20e5b1d9865bf78f7cfc02e60ba0bcf327e0d3949f455a37dd8870979c3fc0f42d87e80300000000000023aa20bcf0c9aabff1ff311bccd16c598ac0bc7860253b2c8535cef71dbb2c0a1a782c87e80300000000000023aa207d3e837c0a706c368bacce2c03d1777a33e0a32a26bbf807c1a9b79446b3769787b0040000000000000f0201008178c99dc8c0c88702000075b0040000000000000f0201008178c99dc8c0c88702000075307500000000000023aa2060935eaee5df8476303b86855a14c093d02aaf816409533ad31ef87e79705b7f872bbdcc1d000000001976a914b093805bb56927c08724b92bfdfd77cf07c2cdc288ac00000000").unwrap()); static PREINIT_TEST_TX02: LazyLock> = LazyLock::new(|| hex::decode("0200000001e2365768e0f14db4f3686481a0f53c9bc8e5f5093b858503744c42605653a99b0b0000006441cb694ca0293e4c34ebaf8339aa81b32c9965fea6503e1ef5748f6c1d7b6cfce8904643e1cb4d09458b44004a632dbd103bc14dc1faaebfaaf5cdf7cba9c94f1c612103b67517abb88bd00b5d81154aeab86a2e65ab952e5d24d178e1c3ea82112834fc000000000c0000000000000000d86a4cbb4361756c64726f6e49646f30072d5fbecfa1bc37a13f58b188f50a5bc1dfb9ab8ae0bf5d3d9a63f6f81efc0b809698001b948240c9cfaa8224383db16e4735efa09c9426d1713d265e849a8e91f2669afefc0a6a0000c027090000000000c027090000000000002d310180969800a92669e1030000000000000000000000c027090000000000000000000000000000000000000000000000000000000000000000001027000080c3c9010010a5d4e800000000e8764817000000811976a91495570c60bbdeaf648a259bb177911169e060a76788ace80300000000000023aa200b8614ab1785d536b190f9244cb8eac7e4139bac23c9cd198a48d987cd94196287e80300000000000023aa201c8817a666e4a1ede4afafd752fd372f7271c3a2036507b29902e9ea1835f27587e80300000000000023aa208abe922d98f699c05865a769eeeb05300e7d74982d0b38ba8c03774db8bc257c87e80300000000000023aa20f69ff67df71b9ecd4b6b39af393551468bb2a46fb261e2e5a61cfb27dbcbb25087e80300000000000023aa20e5b1d9865bf78f7cfc02e60ba0bcf327e0d3949f455a37dd8870979c3fc0f42d87e80300000000000023aa20bcf0c9aabff1ff311bccd16c598ac0bc7860253b2c8535cef71dbb2c0a1a782c87e80300000000000023aa206ead0ce800334c2decb31c9f6b4d3a6a1bf03789513206a107b464481c917bb687b0040000000000000f0201008178c99dc8c0c88702000075b004000000000000c70201008178c99dc8c0c8874cb90c0602c400024a0104011901526a0442434d52206f6465e0112ce788c1ac118d7783add22cc3d0d28b3e3616b89be097cd6919b84968747470733a2f2f697066732e72696674656e2e6e65742f75415655534947396b5a6541524c4f6549776177526a5865447264497377394453697a343246726962344a664e61526d3438697066733a2f2f75415655534947396b5a6541524c4f6549776177526a5865447264497377394453697a343246726962344a664e61526d34b70075307500000000000023aa2060935eaee5df8476303b86855a14c093d02aaf816409533ad31ef87e79705b7f87a91ecc1d000000001976a914b093805bb56927c08724b92bfdfd77cf07c2cdc288ac00000000").unwrap()); #[test] fn preinit_detect() { let tx: Transaction = bitcoincash::consensus::deserialize(&PREINIT_TEST_TX01).expect("should be valid"); assert!(is_preinit_broadcast(&tx)); } #[test] fn parse_ido_preinit_from_valid_ido() { let tx: Transaction = bitcoincash::consensus::deserialize(&PREINIT_TEST_TX01).expect("should be valid"); let mut errors: Vec = Vec::new(); let mut invalid_ido_reasons: Vec = Vec::new(); let network = Some(Network::Chipnet); let result = parse_ido_preinit_tx(network, &tx, &mut errors, &mut invalid_ido_reasons); if errors.len() > 0 { println!("Parse ido preinit failed, txid: {}\nError(s):", hex::encode(tx.txid().to_blob())); for error in &errors { println!(" - {}", error); } } if invalid_ido_reasons.len() > 0 { println!("Invalid ido found, preinit_txid: {}\nReason(s):", hex::encode(tx.txid().to_blob())); for reason in &invalid_ido_reasons { println!(" - {}", reason); } } let params = result.parameters.expect("parameters should be defined!"); let state = result.state.expect("state should be defined!"); println!("params: {}", String::from_utf8(serde_json::to_vec_pretty(¶ms).unwrap()).unwrap()); println!("state: {}", String::from_utf8(serde_json::to_vec_pretty(&state).unwrap()).unwrap()); assert!(errors.len() == 0 && invalid_ido_reasons.len() == 0); assert!(result.is_valid_ido); } #[test] fn parse_ido_preinit_from_valid_ido_with_bcmr() { let tx: Transaction = bitcoincash::consensus::deserialize(&PREINIT_TEST_TX02).expect("should be valid"); let mut errors: Vec = Vec::new(); let mut invalid_ido_reasons: Vec = Vec::new(); let network = Some(Network::Chipnet); let result = parse_ido_preinit_tx(network, &tx, &mut errors, &mut invalid_ido_reasons); if errors.len() > 0 { println!("Parse ido preinit failed, txid: {}\nError(s):", hex::encode(tx.txid().to_blob())); for error in &errors { println!(" - {}", error); } } if invalid_ido_reasons.len() > 0 { println!("Invalid ido found, preinit_txid: {}\nReason(s):", hex::encode(tx.txid().to_blob())); for reason in &invalid_ido_reasons { println!(" - {}", reason); } } let params = result.parameters.expect("parameters should be defined!"); let state = result.state.expect("state should be defined!"); println!("params: {}", String::from_utf8(serde_json::to_vec_pretty(¶ms).unwrap()).unwrap()); println!("state: {}", String::from_utf8(serde_json::to_vec_pretty(&state).unwrap()).unwrap()); assert!(errors.len() == 0 && invalid_ido_reasons.len() == 0); assert!(result.is_valid_ido); } } // ─── Public RPC query functions ────────────────────────────────────────────── pub async fn lookup_internal_id_by_preinit_txid( pool: &SqlitePool, preinit_txid: &[u8], ) -> Result> { let row = sqlx::query("SELECT internal_id FROM ido WHERE preinit_txid = ?") .bind(preinit_txid) .fetch_optional(pool) .await?; Ok(row.map(|r| r.get(0))) } 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 internal_id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, \ status, parameters, state, txchain_entrypoint, txchain_head, is_valid, is_token_created_at_preinit 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 internal_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 internal_id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, \ status, parameters, state, txchain_entrypoint, txchain_head, is_valid, is_token_created_at_preinit \ 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, internal_id: i64, preinit_txid_hex: &str, distributed: Option, offset: i64, limit: i64, ) -> Result> { let mut qb: sqlx::QueryBuilder = sqlx::QueryBuilder::new( "SELECT txid, owner_nfthash, commitment, supply_amount, demand_amount, \ lockup_timeval, discount, distributed FROM ido_entry WHERE ido_id = ", ); qb.push_bind(internal_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(0); let owner_nfthash: Vec = row.get(1); let commitment: Option> = row.get(2); let distributed_int: i64 = row.get(7); Ok(IdoEntryRpcRecord { ido_id: preinit_txid_hex.to_string(), txid: blob_to_display_hex::(&txid_blob)?, owner_nfthash: hex::encode(&owner_nfthash), commitment: commitment.map(|c| hex::encode(&c)), supply_amount: row.get(3), demand_amount: row.get(4), lockup_timeval: row.get(5), discount: row.get(6), distributed: distributed_int != 0, }) }) .collect() } pub async fn list_ido_txchain( pool: &SqlitePool, internal_id: i64, preinit_txid_hex: &str, offset: i64, limit: i64, ) -> Result> { let head_id: Option = sqlx::query("SELECT txchain_head FROM ido WHERE internal_id = ?") .bind(internal_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, txid FROM ido_txchain WHERE ido_id = ?", ) .bind(internal_id) .fetch_all(pool) .await?; // Parse rows into records let records: Vec = rows .iter() .map(|row| { let txid_blob: Vec = row.get(2); Ok(IdoTxChainRpcRecord { id: row.get(0), prev_id: row.get(1), ido_id: preinit_txid_hex.to_string(), ido_internal_id: internal_id, 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 querytests { 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, is_token_created_at_preinit: 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, is_token_created_at_preinit, 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) .bind(is_token_created_at_preinit 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, true, None, None).await; insert_test_ido(&pool, txid(2), "ACTIVE", true, 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, true, None, None).await; insert_test_ido(&pool, txid(2), "PREINIT", false, 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, true, None, None).await; insert_test_ido(&pool, txid(2), "ACTIVE", true, true, None, None).await; insert_test_ido(&pool, txid(3), "ACTIVE", true, 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, true, Some(token.clone()), None).await; insert_test_ido(&pool, txid(2), "ACTIVE", true, 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, 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); // Pages reflect internal_id ascending insertion order: preinit_txid hex of txid(i) bytes // For txid(i) = [i; 32], the display hex is "ii".repeat(32). assert_eq!(page1[0].id, hex::encode(txid(1))); assert_eq!(page1[1].id, hex::encode(txid(2))); assert_eq!(page2[0].id, hex::encode(txid(3))); assert_eq!(page2[1].id, hex::encode(txid(4))); assert_eq!(page3[0].id, hex::encode(txid(5))); } // ─── 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, 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, true, None, None).await; let preinit_hex = hex::encode(txid(1)); 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, &preinit_hex, 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, true, None, None).await; let preinit_hex = hex::encode(txid(1)); 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, &preinit_hex, 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, &preinit_hex, 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, true, None, None).await; let ido2 = insert_test_ido(&pool, txid(2), "ACTIVE", true, true, None, None).await; let preinit_hex_1 = hex::encode(txid(1)); let preinit_hex_2 = hex::encode(txid(2)); 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, &preinit_hex_1, None, 0, 100).await.unwrap(); let e2 = list_ido_entries(&pool, ido2, &preinit_hex_2, 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 internal_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, true, None, None).await; let preinit_hex = hex::encode(txid(1)); 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, &preinit_hex, 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, true, None, None).await; let preinit_hex = hex::encode(txid(1)); 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, &preinit_hex, 0, 2).await.unwrap(); let page2 = list_ido_txchain(&pool, ido_id, &preinit_hex, 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, true, None, None).await; let preinit_hex = hex::encode(txid(1)); // txchain_head is NULL (default from insert_test_ido) let chain = list_ido_txchain(&pool, ido_id, &preinit_hex, 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, "deadbeef", 0, 100).await.unwrap(); assert_eq!(chain.len(), 0); } }